@pushframe/sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +405 -0
  2. package/package.json +3 -2
package/README.md ADDED
@@ -0,0 +1,405 @@
1
+ # @pushframe/sdk
2
+
3
+ React Native SDK for [PushFrame](https://pushframe.io) — server-driven UI rendering engine that fetches and renders UI schemas at runtime without app updates.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @pushframe/sdk
9
+ # or
10
+ yarn add @pushframe/sdk
11
+ ```
12
+
13
+ **Peer dependencies** (must be installed in your project):
14
+
15
+ ```bash
16
+ npm install react react-native
17
+ ```
18
+
19
+ Optionally, if you use `react-native-safe-area-context`, the `safeareaview` component will automatically prefer it over the React Native core fallback.
20
+
21
+ ---
22
+
23
+ ## Quick Start
24
+
25
+ Wrap your app with `PushFrame.Provider`, then drop `PushFrame.Screen` or `PushFrame.Component` wherever you want server-driven UI to appear.
26
+
27
+ ```tsx
28
+ import { PushFrame } from '@pushframe/sdk';
29
+
30
+ export default function App() {
31
+ return (
32
+ <PushFrame.Provider apiKey="your-api-key" appVersion="1.0.0">
33
+ <PushFrame.Screen id="home" />
34
+ </PushFrame.Provider>
35
+ );
36
+ }
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Provider
42
+
43
+ `PushFrame.Provider` is the root wrapper. Place it at the top of your component tree.
44
+
45
+ ```tsx
46
+ <PushFrame.Provider
47
+ apiKey="your-api-key"
48
+ appVersion="1.0.0"
49
+ baseUrl="https://api.pushframe.io"
50
+ context={{ user: { name: 'Alice', isAdmin: true } }}
51
+ components={{ MyCustomCard }}
52
+ loadingComponent={<MySpinner />}
53
+ fallbackComponent={<MyErrorView />}
54
+ onAction={(action, payload) => {
55
+ if (action === 'navigate') {
56
+ navigation.navigate(payload.screen);
57
+ }
58
+ }}
59
+ onError={(error) => console.error(error)}
60
+ >
61
+ {/* your app */}
62
+ </PushFrame.Provider>
63
+ ```
64
+
65
+ ### Provider Props
66
+
67
+ | Prop | Type | Required | Default | Description |
68
+ |------|------|----------|---------|-------------|
69
+ | `apiKey` | `string` | Yes | — | API key used for authentication (`Authorization` and `x-project-key` headers). |
70
+ | `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. |
71
+ | `baseUrl` | `string` | No | `https://api.pushframe.io` | Override the API base URL. |
72
+ | `context` | `Record<string, unknown>` | No | `{}` | Global runtime data available to all binding expressions. |
73
+ | `components` | `Record<string, React.ComponentType>` | No | `{}` | Register custom components by type name, available to the renderer. |
74
+ | `loadingComponent` | `ReactNode` | No | `null` | Shown while a schema is being fetched. |
75
+ | `fallbackComponent` | `ReactNode` | No | `null` | Shown when a schema fetch fails. |
76
+ | `onAction` | `(action: string, payload?) => void` | No | — | Called for any action that is not handled internally (see [Actions](#actions)). |
77
+ | `onError` | `(error: Error) => void` | No | — | Called when a schema fetch or render error occurs. |
78
+ | `children` | `ReactNode` | Yes | — | Your application content. |
79
+
80
+ ---
81
+
82
+ ## Slot Components
83
+
84
+ ### `PushFrame.Screen`
85
+
86
+ Fetches and renders a full-page screen schema. Uses `flex: 1` layout.
87
+
88
+ ```tsx
89
+ <PushFrame.Screen
90
+ id="home"
91
+ context={{ currentTab: 'feed' }}
92
+ onAction={(action, payload) => {
93
+ if (action === 'navigate') {
94
+ navigation.navigate(payload.screen);
95
+ return true; // stop bubbling to Provider
96
+ }
97
+ }}
98
+ />
99
+ ```
100
+
101
+ Fetches from: `GET {baseUrl}/screens/{id}/{appVersion}`
102
+
103
+ ### `PushFrame.Component`
104
+
105
+ Fetches and renders an inline component schema. Renders inline without forced flex.
106
+
107
+ ```tsx
108
+ <PushFrame.Component
109
+ id="product-card"
110
+ context={{ productId: '123' }}
111
+ />
112
+ ```
113
+
114
+ Fetches from: `GET {baseUrl}/components/{id}/{appVersion}`
115
+
116
+ ### Slot Props
117
+
118
+ Both components share these props:
119
+
120
+ | Prop | Type | Required | Description |
121
+ |------|------|----------|-------------|
122
+ | `id` | `string` | Yes | Screen or component ID. |
123
+ | `context` | `Record<string, unknown>` | No | Local context merged with (and overriding) the Provider context. |
124
+ | `isLoading` | `boolean` | No | Externally controlled loading state. |
125
+ | `loadingComponent` | `ReactNode` | No | Overrides the Provider-level loading UI. |
126
+ | `fallbackComponent` | `ReactNode` | No | Overrides the Provider-level fallback UI. |
127
+ | `onAction` | `(action, payload?) => boolean \| void` | No | Local action handler. Return `true` to stop the action from bubbling to the Provider. |
128
+
129
+ ---
130
+
131
+ ## Schema Format
132
+
133
+ The API server must return schemas in this envelope format:
134
+
135
+ ```json
136
+ {
137
+ "schema": {
138
+ "type": "view",
139
+ "props": { "style": { "flex": 1, "padding": 16 } },
140
+ "children": [
141
+ {
142
+ "type": "text",
143
+ "props": { "value": "Hello {{user.name}}" }
144
+ }
145
+ ]
146
+ }
147
+ }
148
+ ```
149
+
150
+ ### SchemaNode
151
+
152
+ ```typescript
153
+ interface SchemaNode {
154
+ id?: string;
155
+ type: string; // Component type (see built-in types below)
156
+ props?: Record<string, unknown>; // Props passed to the component
157
+ children?: SchemaNode[]; // Nested nodes
158
+ actions?: Action[]; // Event → action mappings
159
+ if?: string; // Binding expression; falsy = node is hidden
160
+ }
161
+
162
+ interface Action {
163
+ trigger: string; // e.g. "onPress", "onChange", "onLongPress"
164
+ action: string; // Action name (built-in or custom)
165
+ payload?: Record<string, unknown>;
166
+ }
167
+ ```
168
+
169
+ ### Built-in Component Types
170
+
171
+ | Type | React Native Equivalent | Notes |
172
+ |------|------------------------|-------|
173
+ | `view` | `View` | Layout container |
174
+ | `scrollview` | `ScrollView` | Scrollable container; supports `scroll-to` action |
175
+ | `text` | `Text` | Use `value` prop for text content |
176
+ | `image` | `Image` | Use `src` prop for URI strings |
177
+ | `pressable` | `Pressable` | Triggers `onPress`, `onLongPress` |
178
+ | `textinput` | `TextInput` | `onChange` → `onChangeText`, `onSubmit` → `onSubmitEditing` |
179
+ | `flatlist` | `FlatList` | Requires `items` array and `renderItem` node (see below) |
180
+ | `modal` | `Modal` | |
181
+ | `activityindicator` | `ActivityIndicator` | Spinner |
182
+ | `switch` | `Switch` | `onChange` → `onValueChange` |
183
+ | `keyboardavoidingview` | `KeyboardAvoidingView` | |
184
+ | `safeareaview` | `SafeAreaView` | Prefers `react-native-safe-area-context` when available |
185
+ | `statusbar` | `StatusBar` | |
186
+
187
+ ---
188
+
189
+ ## Data Binding
190
+
191
+ Props support `{{expression}}` syntax resolved against the runtime context.
192
+
193
+ **Full binding** — resolves to the actual value (any type):
194
+ ```json
195
+ { "type": "text", "props": { "value": "{{user.name}}" } }
196
+ ```
197
+
198
+ **Inline binding** — string interpolation:
199
+ ```json
200
+ { "type": "text", "props": { "value": "Hello, {{user.name}}!" } }
201
+ ```
202
+
203
+ **Nested paths:**
204
+ ```json
205
+ { "type": "image", "props": { "src": "{{product.imageUrl}}" } }
206
+ ```
207
+
208
+ Unresolved paths silently return `undefined` — they never cause a crash.
209
+
210
+ ---
211
+
212
+ ## Conditional Rendering
213
+
214
+ Use the `if` field with a binding expression to show or hide a node:
215
+
216
+ ```json
217
+ {
218
+ "type": "text",
219
+ "props": { "value": "Admin Panel" },
220
+ "if": "{{user.isAdmin}}"
221
+ }
222
+ ```
223
+
224
+ Falsy values (`false`, `null`, `undefined`, `0`, `""`) hide the node and its entire subtree.
225
+
226
+ ---
227
+
228
+ ## Actions
229
+
230
+ Actions link UI events to behaviour.
231
+
232
+ ```json
233
+ {
234
+ "type": "pressable",
235
+ "actions": [
236
+ {
237
+ "trigger": "onPress",
238
+ "action": "show-toast",
239
+ "payload": { "message": "Saved!", "type": "success" }
240
+ }
241
+ ],
242
+ "children": [
243
+ { "type": "text", "props": { "value": "Save" } }
244
+ ]
245
+ }
246
+ ```
247
+
248
+ ### Built-in Actions
249
+
250
+ These are handled internally and do **not** reach your `onAction` callback.
251
+
252
+ | Action | Payload |
253
+ |--------|---------|
254
+ | `show-toast` | `message: string`, `duration?: number` (ms), `type?: "success" \| "error" \| "info" \| "warning"` |
255
+ | `show-bottom-sheet` | `schema: SchemaNode`, `context?: Record<string, unknown>` |
256
+ | `dismiss-bottom-sheet` | *(none)* |
257
+ | `scroll-to` | `x?: number`, `y?: number`, `animated?: boolean` |
258
+
259
+ All other action names bubble to the slot's `onAction`, then to the Provider's `onAction` if the slot handler does not return `true`.
260
+
261
+ ---
262
+
263
+ ## FlatList
264
+
265
+ `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.
266
+
267
+ ```json
268
+ {
269
+ "type": "flatlist",
270
+ "props": {
271
+ "items": "{{products}}",
272
+ "direction": "vertical"
273
+ },
274
+ "renderItem": {
275
+ "type": "view",
276
+ "props": { "style": { "padding": 8 } },
277
+ "children": [
278
+ { "type": "text", "props": { "value": "{{item.name}}" } },
279
+ { "type": "text", "props": { "value": "{{item.price}}" } }
280
+ ]
281
+ }
282
+ }
283
+ ```
284
+
285
+ | Prop | Type | Description |
286
+ |------|------|-------------|
287
+ | `items` | `array \| string` | Array of data items (or binding expression resolving to one). |
288
+ | `direction` | `"vertical" \| "horizontal"` | Maps to the `horizontal` prop. Default: `"vertical"`. |
289
+ | `numColumns` | `number` | Passed through to `FlatList`. |
290
+ | `keyExtractor` | `string` | Binding expression evaluated per item (e.g. `"{{item.id}}"`). |
291
+
292
+ ---
293
+
294
+ ## Custom Components
295
+
296
+ Register your own React Native components and reference them by type name in schemas.
297
+
298
+ ```tsx
299
+ import { PushFrame } from '@pushframe/sdk';
300
+
301
+ function ProductCard({ title, price }: { title: string; price: string }) {
302
+ return (
303
+ <View>
304
+ <Text>{title}</Text>
305
+ <Text>{price}</Text>
306
+ </View>
307
+ );
308
+ }
309
+
310
+ <PushFrame.Provider
311
+ apiKey="..."
312
+ components={{ 'product-card': ProductCard }}
313
+ >
314
+ {/* Schema can now use "type": "product-card" */}
315
+ </PushFrame.Provider>
316
+ ```
317
+
318
+ Built-in type names are reserved and cannot be overridden.
319
+
320
+ ---
321
+
322
+ ## Context Merging
323
+
324
+ Context flows from Provider → Slot, with the Slot's context taking precedence on key conflicts.
325
+
326
+ ```tsx
327
+ // Provider sets global context
328
+ <PushFrame.Provider context={{ user: { name: 'Alice' }, theme: 'dark' }}>
329
+ {/* Slot adds/overrides local context */}
330
+ <PushFrame.Screen
331
+ id="profile"
332
+ context={{ pageTitle: 'My Profile', theme: 'light' }} // overrides theme
333
+ />
334
+ </PushFrame.Provider>
335
+ ```
336
+
337
+ ---
338
+
339
+ ## TypeScript
340
+
341
+ The SDK is written in TypeScript. Key types are exported for use in your own code:
342
+
343
+ ```typescript
344
+ import type {
345
+ SchemaNode,
346
+ Action,
347
+ PushFrameProviderProps,
348
+ PushFrameSlotProps,
349
+ ToastPayload,
350
+ BottomSheetPayload,
351
+ } from '@pushframe/sdk';
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Advanced: Accessing Context
357
+
358
+ Use the `usePushFrameContext` hook inside any component rendered within `PushFrame.Provider` to access SDK internals.
359
+
360
+ ```tsx
361
+ import { usePushFrameContext } from '@pushframe/sdk';
362
+
363
+ function MyButton() {
364
+ const { showToast } = usePushFrameContext();
365
+ return (
366
+ <Button onPress={() => showToast({ message: 'Hello!', type: 'info' })} />
367
+ );
368
+ }
369
+ ```
370
+
371
+ ---
372
+
373
+ ## API Reference
374
+
375
+ ### Request Format
376
+
377
+ Every schema fetch includes these headers:
378
+
379
+ ```
380
+ Authorization: Bearer {apiKey}
381
+ x-project-key: {apiKey}
382
+ Accept: application/json
383
+ ```
384
+
385
+ 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`).
386
+
387
+ ### Response Format
388
+
389
+ ```json
390
+ {
391
+ "schema": { },
392
+ "version": 28,
393
+ "status": "published",
394
+ "publishedAt": "2024-01-01T00:00:00Z",
395
+ "targetMinVersion": "0.0.0"
396
+ }
397
+ ```
398
+
399
+ The SDK extracts `response.schema` automatically. A bare `SchemaNode` (without the envelope) is also accepted as a fallback.
400
+
401
+ ---
402
+
403
+ ## License
404
+
405
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushframe/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Pushframe React Native SDK — Server-Driven UI rendering engine",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -13,7 +13,8 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
- "dist"
16
+ "dist",
17
+ "README.md"
17
18
  ],
18
19
  "peerDependencies": {
19
20
  "react": ">=18.0.0",