npm package: @pushframe/sdk
Description: Pushframe React Native SDK — Server-Driven UI rendering engine
Latest version: 0.1.1

# @pushframe/sdk

React Native SDK for [PushFrame](https://pushframe.io) — server-driven UI rendering engine that fetches and renders UI schemas at runtime without app updates.

## Installation

```bash
npm install @pushframe/sdk
# or
yarn add @pushframe/sdk
```

**Peer dependencies** (must be installed in your project):

```bash
npm install react react-native
```

Optionally, if you use `react-native-safe-area-context`, the `safeareaview` component will automatically prefer it over the React Native core fallback.

---

## Quick Start

Wrap your app with `PushFrame.Provider`, then drop `PushFrame.Screen` or `PushFrame.Component` wherever you want server-driven UI to appear.

```tsx
import { PushFrame } from '@pushframe/sdk';

export default function App() {
  return (
    <PushFrame.Provider apiKey="your-api-key" appVersion="1.0.0">
      <PushFrame.Screen id="home" />
    </PushFrame.Provider>
  );
}
```

---

## Provider

`PushFrame.Provider` is the root wrapper. Place it at the top of your component tree.

```tsx
<PushFrame.Provider
  apiKey="your-api-key"
  appVersion="1.0.0"
  baseUrl="https://api.pushframe.io"
  context={{ user: { name: 'Alice', isAdmin: true } }}
  components={{ MyCustomCard }}
  loadingComponent={<MySpinner />}
  fallbackComponent={<MyErrorView />}
  onAction={(action, payload) => {
    if (action === 'navigate') {
      navigation.navigate(payload.screen);
    }
  }}
  onError={(error) => console.error(error)}
>
  {/* your app */}
</PushFrame.Provider>
```

### Provider Props

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `apiKey` | `string` | Yes | — | API key used for authentication (`Authorization` and `x-project-key` headers). |
| `appVersion` | `string` | No | `undefined` | Semver version string sent in fetch URL path. When omitted, the literal `"null"` is sent. Pass this explicitly — auto-detection is not supported. |
| `baseUrl` | `string` | No | `https://api.pushframe.io` | Override the API base URL. |
| `context` | `Record<string, unknown>` | No | `{}` | Global runtime data available to all binding expressions. |
| `components` | `Record<string, React.ComponentType>` | No | `{}` | Register custom components by type name, available to the renderer. |
| `loadingComponent` | `ReactNode` | No | `null` | Shown while a schema is being fetched. |
| `fallbackComponent` | `ReactNode` | No | `null` | Shown when a schema fetch fails. |
| `onAction` | `(action: string, payload?) => void` | No | — | Called for any action that is not handled internally (see [Actions](#actions)). |
| `onError` | `(error: Error) => void` | No | — | Called when a schema fetch or render error occurs. |
| `children` | `ReactNode` | Yes | — | Your application content. |

---

## Slot Components

### `PushFrame.Screen`

Fetches and renders a full-page screen schema. Uses `flex: 1` layout.

```tsx
<PushFrame.Screen
  id="home"
  context={{ currentTab: 'feed' }}
  onAction={(action, payload) => {
    if (action === 'navigate') {
      navigation.navigate(payload.screen);
      return true; // stop bubbling to Provider
    }
  }}
/>
```

Fetches from: `GET {baseUrl}/screens/{id}/{appVersion}`

### `PushFrame.Component`

Fetches and renders an inline component schema. Renders inline without forced flex.

```tsx
<PushFrame.Component
  id="product-card"
  context={{ productId: '123' }}
/>
```

Fetches from: `GET {baseUrl}/components/{id}/{appVersion}`

### Slot Props

Both components share these props:

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `string` | Yes | Screen or component ID. |
| `context` | `Record<string, unknown>` | No | Local context merged with (and overriding) the Provider context. |
| `isLoading` | `boolean` | No | Externally controlled loading state. |
| `loadingComponent` | `ReactNode` | No | Overrides the Provider-level loading UI. |
| `fallbackComponent` | `ReactNode` | No | Overrides the Provider-level fallback UI. |
| `onAction` | `(action, payload?) => boolean \| void` | No | Local action handler. Return `true` to stop the action from bubbling to the Provider. |

---

## Schema Format

The API server must return schemas in this envelope format:

```json
{
  "schema": {
    "type": "view",
    "props": { "style": { "flex": 1, "padding": 16 } },
    "children": [
      {
        "type": "text",
        "props": { "value": "Hello {{user.name}}" }
      }
    ]
  }
}
```

### SchemaNode

```typescript
interface SchemaNode {
  id?: string;
  type: string;                        // Component type (see built-in types below)
  props?: Record<string, unknown>;     // Props passed to the component
  children?: SchemaNode[];             // Nested nodes
  actions?: Action[];                  // Event → action mappings
  if?: string;                         // Binding expression; falsy = node is hidden
}

interface Action {
  trigger: string;   // e.g. "onPress", "onChange", "onLongPress"
  action: string;    // Action name (built-in or custom)
  payload?: Record<string, unknown>;
}
```

### Built-in Component Types

| Type | React Native Equivalent | Notes |
|------|------------------------|-------|
| `view` | `View` | Layout container |
| `scrollview` | `ScrollView` | Scrollable container; supports `scroll-to` action |
| `text` | `Text` | Use `value` prop for text content |
| `image` | `Image` | Use `src` prop for URI strings |
| `pressable` | `Pressable` | Triggers `onPress`, `onLongPress` |
| `textinput` | `TextInput` | `onChange` → `onChangeText`, `onSubmit` → `onSubmitEditing` |
| `flatlist` | `FlatList` | Requires `items` array and `renderItem` node (see below) |
| `modal` | `Modal` | |
| `activityindicator` | `ActivityIndicator` | Spinner |
| `switch` | `Switch` | `onChange` → `onValueChange` |
| `keyboardavoidingview` | `KeyboardAvoidingView` | |
| `safeareaview` | `SafeAreaView` | Prefers `react-native-safe-area-context` when available |
| `statusbar` | `StatusBar` | |

---

## Data Binding

Props support `{{expression}}` syntax resolved against the runtime context.

**Full binding** — resolves to the actual value (any type):
```json
{ "type": "text", "props": { "value": "{{user.name}}" } }
```

**Inline binding** — string interpolation:
```json
{ "type": "text", "props": { "value": "Hello, {{user.name}}!" } }
```

**Nested paths:**
```json
{ "type": "image", "props": { "src": "{{product.imageUrl}}" } }
```

Unresolved paths silently return `undefined` — they never cause a crash.

---

## Conditional Rendering

Use the `if` field with a binding expression to show or hide a node:

```json
{
  "type": "text",
  "props": { "value": "Admin Panel" },
  "if": "{{user.isAdmin}}"
}
```

Falsy values (`false`, `null`, `undefined`, `0`, `""`) hide the node and its entire subtree.

---

## Actions

Actions link UI events to behaviour.

```json
{
  "type": "pressable",
  "actions": [
    {
      "trigger": "onPress",
      "action": "show-toast",
      "payload": { "message": "Saved!", "type": "success" }
    }
  ],
  "children": [
    { "type": "text", "props": { "value": "Save" } }
  ]
}
```

### Built-in Actions

These are handled internally and do **not** reach your `onAction` callback.

| Action | Payload |
|--------|---------|
| `show-toast` | `message: string`, `duration?: number` (ms), `type?: "success" \| "error" \| "info" \| "warning"` |
| `show-bottom-sheet` | `schema: SchemaNode`, `context?: Record<string, unknown>` |
| `dismiss-bottom-sheet` | *(none)* |
| `scroll-to` | `x?: number`, `y?: number`, `animated?: boolean` |

All other action names bubble to the slot's `onAction`, then to the Provider's `onAction` if the slot handler does not return `true`.

---

## FlatList

`flatlist` nodes require an `items` array in props and a `renderItem` node as a sibling field (not inside `children`). The renderer injects `{ item, index }` into the context for each rendered item.

```json
{
  "type": "flatlist",
  "props": {
    "items": "{{products}}",
    "direction": "vertical"
  },
  "renderItem": {
    "type": "view",
    "props": { "style": { "padding": 8 } },
    "children": [
      { "type": "text", "props": { "value": "{{item.name}}" } },
      { "type": "text", "props": { "value": "{{item.price}}" } }
    ]
  }
}
```

| Prop | Type | Description |
|------|------|-------------|
| `items` | `array \| string` | Array of data items (or binding expression resolving to one). |
| `direction` | `"vertical" \| "horizontal"` | Maps to the `horizontal` prop. Default: `"vertical"`. |
| `numColumns` | `number` | Passed through to `FlatList`. |
| `keyExtractor` | `string` | Binding expression evaluated per item (e.g. `"{{item.id}}"`). |

---

## Custom Components

Register your own React Native components and reference them by type name in schemas.

```tsx
import { PushFrame } from '@pushframe/sdk';

function ProductCard({ title, price }: { title: string; price: string }) {
  return (
    <View>
      <Text>{title}</Text>
      <Text>{price}</Text>
    </View>
  );
}

<PushFrame.Provider
  apiKey="..."
  components={{ 'product-card': ProductCard }}
>
  {/* Schema can now use "type": "product-card" */}
</PushFrame.Provider>
```

Built-in type names are reserved and cannot be overridden.

---

## Context Merging

Context flows from Provider → Slot, with the Slot's context taking precedence on key conflicts.

```tsx
// Provider sets global context
<PushFrame.Provider context={{ user: { name: 'Alice' }, theme: 'dark' }}>
  {/* Slot adds/overrides local context */}
  <PushFrame.Screen
    id="profile"
    context={{ pageTitle: 'My Profile', theme: 'light' }} // overrides theme
  />
</PushFrame.Provider>
```

---

## TypeScript

The SDK is written in TypeScript. Key types are exported for use in your own code:

```typescript
import type {
  SchemaNode,
  Action,
  PushFrameProviderProps,
  PushFrameSlotProps,
  ToastPayload,
  BottomSheetPayload,
} from '@pushframe/sdk';
```

---

## Advanced: Accessing Context

Use the `usePushFrameContext` hook inside any component rendered within `PushFrame.Provider` to access SDK internals.

```tsx
import { usePushFrameContext } from '@pushframe/sdk';

function MyButton() {
  const { showToast } = usePushFrameContext();
  return (
    <Button onPress={() => showToast({ message: 'Hello!', type: 'info' })} />
  );
}
```

---

## API Reference

### Request Format

Every schema fetch includes these headers:

```
Authorization: Bearer {apiKey}
x-project-key: {apiKey}
Accept: application/json
```

IDs and `appVersion` are `encodeURIComponent`-encoded in the URL path. When `appVersion` is not set on the Provider, the literal string `"null"` is sent (e.g. `GET /screens/home/null`).

### Response Format

```json
{
  "schema": { },
  "version": 28,
  "status": "published",
  "publishedAt": "2024-01-01T00:00:00Z",
  "targetMinVersion": "0.0.0"
}
```

The SDK extracts `response.schema` automatically. A bare `SchemaNode` (without the envelope) is also accepted as a fallback.

---

## License

MIT