npm package: @openmobile/sdk-web
Description: OpenMobile Web SDK - React components for server-driven UI
Author: OpenMobile Team
Latest version: 0.1.0-alpha

# OpenMobile Web SDK

[![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![npm version](https://img.shields.io/npm/v/@openmobile/sdk-web.svg)](https://www.npmjs.com/package/@openmobile/sdk-web)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://www.typescriptlang.org/)
[![React](https://img.shields.io/badge/React-18.0+-61DAFB)](https://react.dev/)
[![Build Status](https://img.shields.io/github/actions/workflow/status/openmobile/sdk-web/ci.yml?branch=main)](https://github.com/openmobile/sdk-web/actions)

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

```bash
npm install @openmobile/sdk-web
```

### yarn

```bash
yarn add @openmobile/sdk-web
```

### pnpm

```bash
pnpm add @openmobile/sdk-web
```

**Requirements:**
- React 18.0+
- TypeScript 5.0+ (recommended)

---

## 🚀 Quick Start

### 1. Initialize SDK

In your app entry point (`main.tsx` or `App.tsx`):

```typescript
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

```typescript
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

```typescript
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).

```json
{
  "type": "column",
  "properties": {
    "id": "main_column",
    "spacing": 16,
    "padding": 20,
    "scrollable": true,
    "horizontalAlignment": "center",
    "backgroundColor": "#F5F5F5"
  },
  "children": [...]
}
```

### Text

Display text content.

```json
{
  "type": "text",
  "properties": {
    "id": "title",
    "text": "Hello World",
    "fontSize": 24,
    "fontWeight": "bold",
    "color": "#000000",
    "textAlign": "center",
    "maxLines": 2
  }
}
```

### Button

Interactive button.

```json
{
  "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 color
- `secondary`: Filled button with secondary color
- `outline`: Outlined button with transparent background
- `text`: Text-only button (no background, no border)

**Button Sizes:**
- `small`: Compact button
- `medium`: Default size
- `large`: Large button

### Image

Display images from URL.

```json
{
  "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 image
- `contain`: Fits entirely, may have empty space
- `fill`: Stretches to fill container
- `center`: Original size, centered

### Card

Container with elevation (shadow).

```json
{
  "type": "card",
  "properties": {
    "id": "promo_card",
    "elevation": 4,
    "cornerRadius": 16,
    "padding": 20,
    "clickable": true
  },
  "children": [...]
}
```

---

## ⚡ Events

### Navigate

Navigate to another screen/route.

```json
{
  "type": "navigate",
  "route": "/product/detail",
  "params": {
    "product_id": "PROD-123"
  }
}
```

### Open URL

Open external URL or webview.

```json
{
  "type": "open_url",
  "url": "https://example.com",
  "external": true
}
```

### Custom

Trigger custom actions in your app.

```json
{
  "type": "custom",
  "action": "add_to_cart",
  "payload": {
    "product_id": "PROD-123",
    "quantity": 1
  }
}
```

### Submit Form

Submit data to API.

```json
{
  "type": "submit_form",
  "endpoint": "https://api.example.com/contact",
  "method": "POST",
  "fields": {
    "name": "John Doe",
    "email": "john@example.com"
  }
}
```

### Track Analytics

Send analytics events.

```json
{
  "type": "track_analytics",
  "eventName": "button_clicked",
  "properties": {
    "button_id": "cta_primary",
    "screen": "home"
  }
}
```

---

## 🎨 Styling

### Colors

Supports hex colors and named colors:

```json
"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:

```json
"padding": 16,
"margin": 8,
"spacing": 12
```

### Typography

```json
"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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

- [JSON Schema Spec](../../docs/architecture/JSON_SCHEMA.md)
- [Architecture Documentation](../../docs/architecture/ARCHITECTURE.md)
- [Project Context](../../docs/PROJECT_CONTEXT.md)

---

## 🐛 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-core` is installed
- Check `tsconfig.json` has `"moduleResolution": "node"`
- Run `npm install` to ensure all dependencies are installed

---

## 🤝 Contributing

We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

## 📄 License

MIT License - see [LICENSE](LICENSE) file for details.

## 📞 Support

- 📚 [Documentation](https://docs.openmobile.io)
- 🐛 [Report Issues](https://github.com/openmobile/sdk-web/issues)
- 💬 [Discussions](https://github.com/openmobile/sdk-web/discussions)
- 🚀 [Changelog](CHANGELOG.md)

---

**Built with ❤️ by OpenMobile Team**