nn-widgets 0.1.0
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.
- package/README.md +577 -0
- package/android/build.gradle +90 -0
- package/app.plugin.js +4 -0
- package/build/NNWidgets.types.d.ts +113 -0
- package/build/NNWidgets.types.d.ts.map +1 -0
- package/build/NNWidgets.types.js +2 -0
- package/build/NNWidgets.types.js.map +1 -0
- package/build/NNWidgetsModule.d.ts +3 -0
- package/build/NNWidgetsModule.d.ts.map +1 -0
- package/build/NNWidgetsModule.js +3 -0
- package/build/NNWidgetsModule.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +152 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/NNWidgets.podspec +27 -0
- package/ios/NNWidgetsModule.swift +97 -0
- package/package.json +49 -0
- package/plugin/build/index.d.ts +9 -0
- package/plugin/build/index.d.ts.map +1 -0
- package/plugin/build/index.js +70 -0
- package/plugin/build/types.d.ts +353 -0
- package/plugin/build/types.d.ts.map +1 -0
- package/plugin/build/types.js +2 -0
- package/plugin/build/withAndroidWidget.d.ts +5 -0
- package/plugin/build/withAndroidWidget.d.ts.map +1 -0
- package/plugin/build/withAndroidWidget.js +700 -0
- package/plugin/build/withIosWidget.d.ts +6 -0
- package/plugin/build/withIosWidget.d.ts.map +1 -0
- package/plugin/build/withIosWidget.js +1589 -0
- package/plugin/tsconfig.tsbuildinfo +1 -0
- package/publish.sh +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
# nn-widgets
|
|
2
|
+
|
|
3
|
+
Expo config plugin for native widgets — iOS WidgetKit & Android App Widgets.
|
|
4
|
+
|
|
5
|
+
Supports four widget types:
|
|
6
|
+
|
|
7
|
+
| Type | Description | Data Source |
|
|
8
|
+
| ---------- | ------------------------------------------------------- | --------------------------- |
|
|
9
|
+
| `"image"` | Full-bleed background image | Static asset at build time |
|
|
10
|
+
| `"single"` | Icon + Title + Subtitle + Value | `updateWidget()` at runtime |
|
|
11
|
+
| `"list"` | Vertical list of items with icon/title/description | `updateWidget()` at runtime |
|
|
12
|
+
| `"grid"` | Grid of items, each cell with icon on top + title below | `updateWidget()` at runtime |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx expo install nn-widgets
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### 1. Configure in `app.json`
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"plugins": [
|
|
29
|
+
[
|
|
30
|
+
"nn-widgets",
|
|
31
|
+
{
|
|
32
|
+
"widgets": [
|
|
33
|
+
{
|
|
34
|
+
"name": "QuickActions",
|
|
35
|
+
"displayName": "Quick Actions",
|
|
36
|
+
"type": "list",
|
|
37
|
+
"deepLinkUrl": "myapp://widget",
|
|
38
|
+
"widgetFamilies": ["systemMedium", "systemLarge"],
|
|
39
|
+
"fallbackTitle": "Quick Actions",
|
|
40
|
+
"fallbackSubtitle": "Open app to set up",
|
|
41
|
+
"style": {
|
|
42
|
+
"backgroundColor": "#1E1E2E",
|
|
43
|
+
"titleColor": "#CDD6F4",
|
|
44
|
+
"subtitleColor": "#A6ADC8",
|
|
45
|
+
"accentColor": "#F38BA8"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"ios": {
|
|
50
|
+
"devTeam": "YOUR_TEAM_ID",
|
|
51
|
+
"useAppGroups": true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Push data from your app
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { NNWidgets } from "nn-widgets";
|
|
63
|
+
|
|
64
|
+
await NNWidgets.updateWidget("QuickActions", {
|
|
65
|
+
items: [
|
|
66
|
+
{
|
|
67
|
+
icon: "star.fill",
|
|
68
|
+
title: "Morning Focus",
|
|
69
|
+
description: "25 min session",
|
|
70
|
+
deepLink: "myapp://session/123",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
icon: { url: "brain", size: 28, radius: 14, backgroundColor: "#3B3B5C" },
|
|
74
|
+
title: { text: "Brainstorm", color: "#F38BA8", fontWeight: "bold" },
|
|
75
|
+
description: { text: "15 min", fontSize: "caption" },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. Rebuild
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx expo prebuild --clean
|
|
85
|
+
npx expo run:ios
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Plugin Configuration
|
|
91
|
+
|
|
92
|
+
### `widgets[]` — Per-widget config
|
|
93
|
+
|
|
94
|
+
| Property | Type | Default | Description |
|
|
95
|
+
| ------------------ | ------------------------------- | ---------------------------------------------- | ------------------------------------------------- |
|
|
96
|
+
| `name` | `string` | **required** | Unique identifier (valid Swift/Kotlin identifier) |
|
|
97
|
+
| `displayName` | `string` | **required** | Name shown in widget gallery |
|
|
98
|
+
| `description` | `string` | `"A widget for your app"` | Description in widget gallery |
|
|
99
|
+
| `type` | `"single" \| "list" \| "image"` | auto-detect | Widget display type |
|
|
100
|
+
| `deepLinkUrl` | `string` | — | URL opened when widget is tapped |
|
|
101
|
+
| `showAppIcon` | `boolean` | `true` | Show app icon in widget UI |
|
|
102
|
+
| `widgetFamilies` | `string[]` | `["systemSmall","systemMedium","systemLarge"]` | Supported sizes (iOS) |
|
|
103
|
+
| `fallbackTitle` | `string` | `displayName` | Title shown when no data set |
|
|
104
|
+
| `fallbackSubtitle` | `string` | `"Open app to start"` | Subtitle shown when no data set |
|
|
105
|
+
| `image` | `object \| string` | — | Background image paths (image type) |
|
|
106
|
+
| `icons` | `Record<string, string>` | — | Bundled icon images (name → path) |
|
|
107
|
+
| `style` | `object` | — | Widget-level colors |
|
|
108
|
+
| `android` | `object` | — | Android-specific overrides |
|
|
109
|
+
|
|
110
|
+
#### `type` auto-detection
|
|
111
|
+
|
|
112
|
+
- If `image` prop is set → `"image"`
|
|
113
|
+
- Otherwise → `"single"`
|
|
114
|
+
- Set `type: "list"` explicitly for list widgets
|
|
115
|
+
- Set `type: "grid"` explicitly for grid widgets
|
|
116
|
+
|
|
117
|
+
#### `gridLayout` (for type `"grid"`)
|
|
118
|
+
|
|
119
|
+
Format: `"COLUMNSxROWS"` — controls the grid dimensions.
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{ "gridLayout": "3x2" }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Value | Columns | Rows | Max Items |
|
|
126
|
+
| ------- | ------- | ---- | --------- |
|
|
127
|
+
| `"2x2"` | 2 | 2 | 4 |
|
|
128
|
+
| `"3x2"` | 3 | 2 | 6 |
|
|
129
|
+
| `"4x2"` | 4 | 2 | 8 |
|
|
130
|
+
| `"2x3"` | 2 | 3 | 6 |
|
|
131
|
+
|
|
132
|
+
If omitted, columns auto-adjust based on widget family:
|
|
133
|
+
- `systemSmall`: 2 columns
|
|
134
|
+
- `systemMedium`: 4 columns
|
|
135
|
+
- `systemLarge`: 4 columns
|
|
136
|
+
|
|
137
|
+
#### `image` (for type `"image"`)
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"image": {
|
|
142
|
+
"small": "./assets/widgets/bg-small.png",
|
|
143
|
+
"medium": "./assets/widgets/bg-medium.png",
|
|
144
|
+
"large": "./assets/widgets/bg-large.png"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or a single image for all sizes: `"image": "./assets/widgets/bg.png"`
|
|
150
|
+
|
|
151
|
+
#### `icons` (for type `"list"` / `"single"`)
|
|
152
|
+
|
|
153
|
+
Bundle icon images at build time. Referenced by name at runtime.
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"icons": {
|
|
158
|
+
"brainstorm": "./assets/widgets/icons/brainstorm.png",
|
|
159
|
+
"meditation": "./assets/widgets/icons/meditation.png",
|
|
160
|
+
"focus": "./assets/widgets/icons/focus.png"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
At runtime, use the icon name:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
{ icon: 'brainstorm', title: 'Morning Focus' }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
On iOS, the icon is checked: if a bundled image matches the name, it's used as `Image(name)`. Otherwise, it's treated as an SF Symbol (`Image(systemName:)`).
|
|
172
|
+
|
|
173
|
+
#### `style`
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"style": {
|
|
178
|
+
"backgroundColor": "#1E1E2E",
|
|
179
|
+
"titleColor": "#CDD6F4",
|
|
180
|
+
"subtitleColor": "#A6ADC8",
|
|
181
|
+
"accentColor": "#F38BA8"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
These provide widget-level defaults. Items can override per-item via `WidgetItemText` objects.
|
|
187
|
+
|
|
188
|
+
### `ios` — Shared iOS config
|
|
189
|
+
|
|
190
|
+
| Property | Type | Default | Description |
|
|
191
|
+
| -------------------- | --------- | ------------------ | ---------------------------------------- |
|
|
192
|
+
| `deploymentTarget` | `string` | `"17.0"` | Minimum iOS version for widget extension |
|
|
193
|
+
| `useAppGroups` | `boolean` | `true` | Enable App Groups for data sharing |
|
|
194
|
+
| `appGroupIdentifier` | `string` | `group.{bundleId}` | Custom App Group ID |
|
|
195
|
+
| `devTeam` | `string` | — | Apple development team ID |
|
|
196
|
+
|
|
197
|
+
> **Important:** `useAppGroups: true` requires the App Group capability to be configured in your Apple Developer account. For local development without provisioning, set `useAppGroups: false` (widget won't receive data from the app).
|
|
198
|
+
|
|
199
|
+
### `android` — Shared Android config
|
|
200
|
+
|
|
201
|
+
| Property | Type | Default | Description |
|
|
202
|
+
| -------------------- | -------- | ------------------------ | --------------------------------- |
|
|
203
|
+
| `minSdkVersion` | `number` | `26` | Minimum SDK for widget |
|
|
204
|
+
| `updatePeriodMillis` | `number` | `1800000` | Auto-update interval (min 30 min) |
|
|
205
|
+
| `minWidth` | `number` | `110` | Default widget width in dp |
|
|
206
|
+
| `minHeight` | `number` | `40` | Default widget height in dp |
|
|
207
|
+
| `resizeMode` | `string` | `"horizontal\|vertical"` | Resize behavior |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Runtime API
|
|
212
|
+
|
|
213
|
+
### `NNWidgets.updateWidget(widgetName, data)`
|
|
214
|
+
|
|
215
|
+
Push structured data to a specific widget and reload its timeline.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { NNWidgets } from "nn-widgets";
|
|
219
|
+
|
|
220
|
+
// List-type widget
|
|
221
|
+
await NNWidgets.updateWidget("SessionsList", {
|
|
222
|
+
items: [
|
|
223
|
+
{
|
|
224
|
+
icon: "star.fill",
|
|
225
|
+
title: "Morning Focus",
|
|
226
|
+
description: "25 min",
|
|
227
|
+
deepLink: "myapp://session/1",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
icon: { url: "meditation", size: 28, radius: 14 },
|
|
231
|
+
title: { text: "Wind Down", color: "#CDD6F4", fontWeight: "bold" },
|
|
232
|
+
description: { text: "15 min", color: "#A6ADC8", fontSize: "caption" },
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Single-type widget
|
|
238
|
+
await NNWidgets.updateWidget("StatsWidget", {
|
|
239
|
+
title: "Focus Time",
|
|
240
|
+
subtitle: "This week",
|
|
241
|
+
value: 142,
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### `WidgetDataPayload`
|
|
246
|
+
|
|
247
|
+
| Field | Type | Used by | Description |
|
|
248
|
+
| ---------- | ------------------ | ----------------------- | --------------------- |
|
|
249
|
+
| `title` | `string` | single, list (fallback) | Title text |
|
|
250
|
+
| `subtitle` | `string` | single, list (fallback) | Subtitle text |
|
|
251
|
+
| `value` | `number` | single | Numeric display value |
|
|
252
|
+
| `items` | `WidgetListItem[]` | list | Array of list items |
|
|
253
|
+
|
|
254
|
+
#### `WidgetListItem`
|
|
255
|
+
|
|
256
|
+
| Field | Type | Default | Description |
|
|
257
|
+
| ------------- | -------------------------- | ------------ | ------------------------------- |
|
|
258
|
+
| `icon` | `string \| WidgetItemIcon` | — | Left icon (name or config) |
|
|
259
|
+
| `rightIcon` | `string \| WidgetItemIcon` | — | Right icon (name or config) |
|
|
260
|
+
| `title` | `string \| WidgetItemText` | **required** | Title text or styled text |
|
|
261
|
+
| `description` | `string \| WidgetItemText` | — | Description text or styled text |
|
|
262
|
+
| `deepLink` | `string` | — | Per-item deep link URL |
|
|
263
|
+
|
|
264
|
+
#### `WidgetItemIcon` (object form)
|
|
265
|
+
|
|
266
|
+
| Field | Type | Default | Description |
|
|
267
|
+
| ----------------- | -------- | ------------ | ----------------------------------- |
|
|
268
|
+
| `url` | `string` | **required** | SF Symbol name or bundled icon name |
|
|
269
|
+
| `size` | `number` | `32` | Icon size in points/dp |
|
|
270
|
+
| `radius` | `number` | `0` | Corner radius (size/2 = circle) |
|
|
271
|
+
| `backgroundColor` | `string` | — | Background color (hex) |
|
|
272
|
+
|
|
273
|
+
**Icon styling examples:**
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
// Simple SF Symbol (default size 32, no background)
|
|
277
|
+
icon: "star.fill"
|
|
278
|
+
|
|
279
|
+
// Circular icon with background
|
|
280
|
+
icon: { url: "star.fill", size: 40, radius: 20, backgroundColor: "#FFD700" }
|
|
281
|
+
|
|
282
|
+
// Square icon with rounded corners
|
|
283
|
+
icon: { url: "brain", size: 36, radius: 8, backgroundColor: "#3B3B5C" }
|
|
284
|
+
|
|
285
|
+
// Right icon (e.g. chevron indicator)
|
|
286
|
+
rightIcon: { url: "chevron.right", size: 20, radius: 0 }
|
|
287
|
+
|
|
288
|
+
// Right icon as circular badge
|
|
289
|
+
rightIcon: { url: "checkmark.circle.fill", size: 24, radius: 12, backgroundColor: "#4CAF50" }
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Both `icon` (left) and `rightIcon` (right) accept the same format — either a string or a `WidgetItemIcon` object.
|
|
293
|
+
|
|
294
|
+
#### `WidgetItemText` (object form)
|
|
295
|
+
|
|
296
|
+
| Field | Type | Default | Description |
|
|
297
|
+
| ------------ | ------------------ | -------------------------------------------- | ---------------- |
|
|
298
|
+
| `text` | `string` | **required** | Text content |
|
|
299
|
+
| `color` | `string` | widget style | Text color (hex) |
|
|
300
|
+
| `fontSize` | `WidgetFontSize` | `"subheadline"` (title) / `"caption"` (desc) | Font size key |
|
|
301
|
+
| `fontWeight` | `WidgetFontWeight` | `"semibold"` (title) / `"regular"` (desc) | Font weight |
|
|
302
|
+
|
|
303
|
+
### Font Size Reference
|
|
304
|
+
|
|
305
|
+
| Key | iOS (SwiftUI) | Android (sp) |
|
|
306
|
+
| ------------- | ----------------------- | ------------ |
|
|
307
|
+
| `caption2` | `.caption2` (11pt) | 11sp |
|
|
308
|
+
| `caption` | `.caption` (12pt) | 12sp |
|
|
309
|
+
| `footnote` | `.footnote` (13pt) | 13sp |
|
|
310
|
+
| `subheadline` | `.subheadline` (15pt) | 15sp |
|
|
311
|
+
| `body` | `.body` (17pt) | 17sp |
|
|
312
|
+
| `headline` | `.headline` (17pt bold) | 17sp bold |
|
|
313
|
+
| `title3` | `.title3` (20pt) | 20sp |
|
|
314
|
+
| `title2` | `.title2` (22pt) | 22sp |
|
|
315
|
+
| `title` | `.title` (28pt) | 28sp |
|
|
316
|
+
|
|
317
|
+
### Font Weight Reference
|
|
318
|
+
|
|
319
|
+
| Key | iOS | Android |
|
|
320
|
+
| ---------- | ----------- | ----------------------- |
|
|
321
|
+
| `regular` | `.regular` | `Typeface.NORMAL` |
|
|
322
|
+
| `medium` | `.medium` | `Typeface.NORMAL` + 500 |
|
|
323
|
+
| `semibold` | `.semibold` | `Typeface.BOLD` |
|
|
324
|
+
| `bold` | `.bold` | `Typeface.BOLD` |
|
|
325
|
+
|
|
326
|
+
### Max Items per Widget Size (iOS)
|
|
327
|
+
|
|
328
|
+
| Size | Max Items |
|
|
329
|
+
| ------------------ | --------- |
|
|
330
|
+
| `systemSmall` | 3 |
|
|
331
|
+
| `systemMedium` | 3 |
|
|
332
|
+
| `systemLarge` | 7 |
|
|
333
|
+
| `systemExtraLarge` | 7 |
|
|
334
|
+
|
|
335
|
+
### Other API Methods
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
// Legacy flat key-value data (backward compatible)
|
|
339
|
+
await NNWidgets.setWidgetData({ key: "value", count: 42 });
|
|
340
|
+
|
|
341
|
+
// Get stored data
|
|
342
|
+
const data = await NNWidgets.getWidgetData();
|
|
343
|
+
|
|
344
|
+
// Reload a specific widget
|
|
345
|
+
await NNWidgets.reloadWidget({ widgetName: "MyWidget" });
|
|
346
|
+
|
|
347
|
+
// Reload all widgets
|
|
348
|
+
await NNWidgets.reloadWidget();
|
|
349
|
+
|
|
350
|
+
// Check support
|
|
351
|
+
const supported = NNWidgets.isSupported();
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## How Data Flows
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
┌──────────────────────┐
|
|
360
|
+
│ React Native App │
|
|
361
|
+
│ │
|
|
362
|
+
│ NNWidgets.update │
|
|
363
|
+
│ Widget("List", { │
|
|
364
|
+
│ items: [...] │
|
|
365
|
+
│ }) │
|
|
366
|
+
└──────────┬───────────┘
|
|
367
|
+
│
|
|
368
|
+
▼
|
|
369
|
+
┌──────────────────────┐
|
|
370
|
+
│ Native Module │
|
|
371
|
+
│ (NNWidgetsModule) │
|
|
372
|
+
│ │
|
|
373
|
+
│ Stores key-value │
|
|
374
|
+
│ pairs to shared │
|
|
375
|
+
│ storage │
|
|
376
|
+
│ │
|
|
377
|
+
│ iOS: UserDefaults │
|
|
378
|
+
│ (App Groups) │
|
|
379
|
+
│ Android: Shared │
|
|
380
|
+
│ Preferences │
|
|
381
|
+
└──────────┬───────────┘
|
|
382
|
+
│
|
|
383
|
+
▼
|
|
384
|
+
┌──────────────────────┐
|
|
385
|
+
│ Widget Extension │
|
|
386
|
+
│ (TimelineProvider) │
|
|
387
|
+
│ │
|
|
388
|
+
│ Reads items JSON │
|
|
389
|
+
│ from shared storage │
|
|
390
|
+
│ Parses & renders │
|
|
391
|
+
│ the widget UI │
|
|
392
|
+
└──────────────────────┘
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Key Naming Convention
|
|
396
|
+
|
|
397
|
+
Data is stored with the `widget_` prefix:
|
|
398
|
+
|
|
399
|
+
| JS call | Storage key | Widget reads |
|
|
400
|
+
| ------------------------------------------ | --------------------- | --------------------------- |
|
|
401
|
+
| `updateWidget("MyList", { title: "Hi" })` | `widget_MyList_title` | `getString("MyList_title")` |
|
|
402
|
+
| `updateWidget("MyList", { items: [...] })` | `widget_MyList_items` | `getJSON("MyList_items")` |
|
|
403
|
+
| `setWidgetData({ foo: "bar" })` | `widget_foo` | `getString("foo")` |
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Full Example: `app.json`
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"plugins": [
|
|
412
|
+
[
|
|
413
|
+
"nn-widgets",
|
|
414
|
+
{
|
|
415
|
+
"widgets": [
|
|
416
|
+
{
|
|
417
|
+
"name": "MainWidget",
|
|
418
|
+
"displayName": "My App",
|
|
419
|
+
"type": "single",
|
|
420
|
+
"deepLinkUrl": "myapp://widget",
|
|
421
|
+
"showAppIcon": true,
|
|
422
|
+
"widgetFamilies": ["systemSmall"]
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"name": "RecentSessions",
|
|
426
|
+
"displayName": "Recent Sessions",
|
|
427
|
+
"type": "list",
|
|
428
|
+
"deepLinkUrl": "myapp://sessions",
|
|
429
|
+
"widgetFamilies": ["systemMedium", "systemLarge"],
|
|
430
|
+
"fallbackTitle": "Recent Sessions",
|
|
431
|
+
"fallbackSubtitle": "Open app to see sessions",
|
|
432
|
+
"icons": {
|
|
433
|
+
"focus": "./assets/widgets/icons/focus.png",
|
|
434
|
+
"relax": "./assets/widgets/icons/relax.png"
|
|
435
|
+
},
|
|
436
|
+
"style": {
|
|
437
|
+
"backgroundColor": "#1E1E2E",
|
|
438
|
+
"titleColor": "#CDD6F4",
|
|
439
|
+
"subtitleColor": "#A6ADC8",
|
|
440
|
+
"accentColor": "#F38BA8"
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"name": "BannerWidget",
|
|
445
|
+
"displayName": "Banner",
|
|
446
|
+
"deepLinkUrl": "myapp://banner",
|
|
447
|
+
"widgetFamilies": ["systemMedium"],
|
|
448
|
+
"image": {
|
|
449
|
+
"medium": "./assets/widgets/banner.png"
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"name": "QuickGrid",
|
|
454
|
+
"displayName": "Quick Grid",
|
|
455
|
+
"type": "grid",
|
|
456
|
+
"gridLayout": "3x2",
|
|
457
|
+
"deepLinkUrl": "myapp://grid",
|
|
458
|
+
"widgetFamilies": ["systemMedium", "systemLarge"],
|
|
459
|
+
"fallbackTitle": "Quick Grid",
|
|
460
|
+
"fallbackSubtitle": "Open app to set up",
|
|
461
|
+
"style": {
|
|
462
|
+
"backgroundColor": "#1A1A2E",
|
|
463
|
+
"titleColor": "#E0E0E0",
|
|
464
|
+
"accentColor": "#BB86FC"
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
],
|
|
468
|
+
"ios": {
|
|
469
|
+
"devTeam": "YOUR_TEAM_ID",
|
|
470
|
+
"useAppGroups": true,
|
|
471
|
+
"appGroupIdentifier": "group.com.myapp"
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
]
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## Full Example: Pushing data from a screen
|
|
480
|
+
|
|
481
|
+
```tsx
|
|
482
|
+
import React, { useEffect } from 'react';
|
|
483
|
+
import { NNWidgets } from 'nn-widgets';
|
|
484
|
+
|
|
485
|
+
function SessionsScreen({ sessions }) {
|
|
486
|
+
useEffect(() => {
|
|
487
|
+
// Update list widget with styled icons
|
|
488
|
+
NNWidgets.updateWidget('RecentSessions', {
|
|
489
|
+
items: sessions.slice(0, 7).map((s) => ({
|
|
490
|
+
icon: { url: s.type, size: 32, radius: 8, backgroundColor: '#3B3B5C' },
|
|
491
|
+
title: { text: s.name, fontWeight: 'semibold' },
|
|
492
|
+
description: { text: `${s.duration} min`, fontSize: 'caption' },
|
|
493
|
+
rightIcon: { url: 'chevron.right', size: 16 },
|
|
494
|
+
deepLink: `myapp://session/${s.id}`,
|
|
495
|
+
})),
|
|
496
|
+
});
|
|
497
|
+
}, [sessions]);
|
|
498
|
+
|
|
499
|
+
return (/* ... */);
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Full Example: Grid widget data
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
// Grid widget with 3x2 layout (3 columns, 2 rows, max 6 items)
|
|
507
|
+
await NNWidgets.updateWidget('QuickGrid', {
|
|
508
|
+
items: [
|
|
509
|
+
{
|
|
510
|
+
icon: { url: 'star.fill', size: 40, radius: 20, backgroundColor: '#FFD700' },
|
|
511
|
+
title: 'Favorites',
|
|
512
|
+
deepLink: 'myapp://favorites',
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
icon: { url: 'clock.fill', size: 40, radius: 20, backgroundColor: '#4A90D9' },
|
|
516
|
+
title: 'Recent',
|
|
517
|
+
deepLink: 'myapp://recent',
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
icon: { url: 'bolt.fill', size: 40, radius: 20, backgroundColor: '#FF6B6B' },
|
|
521
|
+
title: 'Quick Start',
|
|
522
|
+
deepLink: 'myapp://quick',
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
icon: { url: 'person.2.fill', size: 40, radius: 20, backgroundColor: '#66BB6A' },
|
|
526
|
+
title: 'Team',
|
|
527
|
+
deepLink: 'myapp://team',
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
icon: { url: 'chart.bar.fill', size: 40, radius: 20, backgroundColor: '#AB47BC' },
|
|
531
|
+
title: 'Stats',
|
|
532
|
+
deepLink: 'myapp://stats',
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
icon: { url: 'gearshape.fill', size: 40, radius: 20, backgroundColor: '#78909C' },
|
|
536
|
+
title: 'Settings',
|
|
537
|
+
deepLink: 'myapp://settings',
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## Troubleshooting
|
|
546
|
+
|
|
547
|
+
### Widget shows fallback text ("Open app to start")
|
|
548
|
+
|
|
549
|
+
- The app hasn't called `updateWidget()` yet, or `useAppGroups` is `false`
|
|
550
|
+
- Ensure `useAppGroups: true` in `ios` config and the App Group is provisioned
|
|
551
|
+
|
|
552
|
+
### Widget doesn't update after calling `updateWidget()`
|
|
553
|
+
|
|
554
|
+
- `updateWidget()` automatically calls `reloadWidget()` after storing data
|
|
555
|
+
- iOS may delay widget updates. Force refresh by removing and re-adding the widget
|
|
556
|
+
|
|
557
|
+
### Build error: "App Group not configured"
|
|
558
|
+
|
|
559
|
+
- Add the App Group capability in Apple Developer portal
|
|
560
|
+
- The App Group ID must match `appGroupIdentifier` (default: `group.{bundleIdentifier}`)
|
|
561
|
+
- For local dev without provisioning, set `useAppGroups: false`
|
|
562
|
+
|
|
563
|
+
### Icons not showing
|
|
564
|
+
|
|
565
|
+
- Bundled icons: ensure paths in `icons` config are correct and files exist
|
|
566
|
+
- SF Symbols: use exact symbol names (e.g., `"star.fill"`, not `"star"`)
|
|
567
|
+
- Android: icon names are lowercased for drawable resources
|
|
568
|
+
|
|
569
|
+
## Requirements
|
|
570
|
+
|
|
571
|
+
- Expo SDK 53+
|
|
572
|
+
- iOS 17.0+ (for WidgetKit)
|
|
573
|
+
- Android API 26+ (for App Widgets)
|
|
574
|
+
|
|
575
|
+
## License
|
|
576
|
+
|
|
577
|
+
MIT
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
apply plugin: 'maven-publish'
|
|
4
|
+
|
|
5
|
+
group = 'expo.modules.nnwidgets'
|
|
6
|
+
version = '0.1.0'
|
|
7
|
+
|
|
8
|
+
buildscript {
|
|
9
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
10
|
+
if (expoModulesCorePlugin.exists()) {
|
|
11
|
+
apply from: expoModulesCorePlugin
|
|
12
|
+
applyKotlinExpoModulesCorePlugin()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ext.safeExtGet = { prop, fallback ->
|
|
16
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
ext.getKotlinVersion = {
|
|
20
|
+
if (ext.has("kotlinVersion")) {
|
|
21
|
+
ext.kotlinVersion()
|
|
22
|
+
} else {
|
|
23
|
+
ext.safeExtGet("kotlinVersion", "1.8.10")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
repositories {
|
|
28
|
+
mavenCentral()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dependencies {
|
|
32
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
afterEvaluate {
|
|
37
|
+
publishing {
|
|
38
|
+
publications {
|
|
39
|
+
release(MavenPublication) {
|
|
40
|
+
from components.release
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
repositories {
|
|
44
|
+
maven {
|
|
45
|
+
url = mavenLocal().url
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
android {
|
|
52
|
+
namespace = "expo.modules.nnwidgets"
|
|
53
|
+
|
|
54
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
55
|
+
|
|
56
|
+
compileOptions {
|
|
57
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
58
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
kotlinOptions {
|
|
62
|
+
jvmTarget = JavaVersion.VERSION_17.majorVersion
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
defaultConfig {
|
|
66
|
+
minSdkVersion safeExtGet("minSdkVersion", 26)
|
|
67
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
68
|
+
versionCode 1
|
|
69
|
+
versionName "0.1.0"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
lintOptions {
|
|
73
|
+
abortOnError false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
publishing {
|
|
77
|
+
singleVariant("release") {
|
|
78
|
+
withSourcesJar()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
repositories {
|
|
84
|
+
mavenCentral()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dependencies {
|
|
88
|
+
implementation project(':expo-modules-core')
|
|
89
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
90
|
+
}
|
package/app.plugin.js
ADDED