@proveanything/smartlinks 1.8.6 → 1.8.9
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/dist/api/appConfiguration.d.ts +19 -5
- package/dist/api/appConfiguration.js +10 -3
- package/dist/api/collection.d.ts +8 -1
- package/dist/api/collection.js +8 -1
- package/dist/docs/API_SUMMARY.md +24 -7
- package/dist/docs/ai-guide-template.md +21 -0
- package/dist/docs/app-data-storage.md +44 -1
- package/dist/docs/building-react-components.md +319 -0
- package/dist/docs/containers.md +184 -0
- package/dist/docs/overview.md +30 -0
- package/dist/docs/widgets.md +98 -0
- package/dist/openapi.yaml +3 -3
- package/docs/API_SUMMARY.md +24 -7
- package/docs/ai-guide-template.md +21 -0
- package/docs/app-data-storage.md +44 -1
- package/docs/building-react-components.md +319 -0
- package/docs/containers.md +184 -0
- package/docs/overview.md +30 -0
- package/docs/widgets.md +98 -0
- package/openapi.yaml +3 -3
- package/package.json +1 -1
|
@@ -17,9 +17,16 @@ export type AppConfigOptions = {
|
|
|
17
17
|
batchId?: string;
|
|
18
18
|
/** Item ID - required for getDataItem/deleteDataItem */
|
|
19
19
|
itemId?: string;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Use admin endpoints instead of public.
|
|
22
|
+
* This selects which endpoint is called; it does not by itself make root-level config fields private.
|
|
23
|
+
*/
|
|
21
24
|
admin?: boolean;
|
|
22
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Configuration object for setConfig.
|
|
27
|
+
* For admin-only values in app config, store them under a top-level `admin` object.
|
|
28
|
+
* Public reads return the root config but omit `config.admin`.
|
|
29
|
+
*/
|
|
23
30
|
config?: any;
|
|
24
31
|
/** Data object for setDataItem. Best for small keyed scoped documents rather than richer app domain objects. */
|
|
25
32
|
data?: any;
|
|
@@ -152,6 +159,8 @@ export declare namespace userAppData {
|
|
|
152
159
|
/**
|
|
153
160
|
* Collection/Product-scoped app configuration.
|
|
154
161
|
* This is admin-managed configuration that applies to all users within the scope.
|
|
162
|
+
* Root-level config fields are typically part of the public view; if you need admin-only
|
|
163
|
+
* values, store them under a top-level `admin` object so public reads can omit them.
|
|
155
164
|
*
|
|
156
165
|
* @example
|
|
157
166
|
* ```typescript
|
|
@@ -180,7 +189,10 @@ export declare namespace userAppData {
|
|
|
180
189
|
*/
|
|
181
190
|
export declare namespace appConfiguration {
|
|
182
191
|
/**
|
|
183
|
-
|
|
192
|
+
* Get app configuration for a collection/product scope.
|
|
193
|
+
* Public reads return the public view of the config. If the stored config contains a
|
|
194
|
+
* top-level `admin` object, that block is omitted from public responses and included
|
|
195
|
+
* when `opts.admin === true`.
|
|
184
196
|
*
|
|
185
197
|
* @param opts - Configuration options including appId and scope (collectionId, productId, etc.)
|
|
186
198
|
* @returns The configuration object
|
|
@@ -228,8 +240,10 @@ export declare namespace appConfiguration {
|
|
|
228
240
|
*/
|
|
229
241
|
function listWidgetInstances(opts: Omit<GetWidgetInstanceOptions, 'widgetId'>): Promise<WidgetInstanceSummary[]>;
|
|
230
242
|
/**
|
|
231
|
-
|
|
232
|
-
|
|
243
|
+
* Set app configuration for a collection/product scope.
|
|
244
|
+
* Requires admin authentication.
|
|
245
|
+
* Writing through the admin endpoint does not make every root-level field private.
|
|
246
|
+
* Use `config.admin` for confidential values that should only be returned on admin reads.
|
|
233
247
|
*
|
|
234
248
|
* @param opts - Configuration options including appId, scope, and config data
|
|
235
249
|
* @returns The saved configuration
|
|
@@ -200,6 +200,8 @@ export var userAppData;
|
|
|
200
200
|
/**
|
|
201
201
|
* Collection/Product-scoped app configuration.
|
|
202
202
|
* This is admin-managed configuration that applies to all users within the scope.
|
|
203
|
+
* Root-level config fields are typically part of the public view; if you need admin-only
|
|
204
|
+
* values, store them under a top-level `admin` object so public reads can omit them.
|
|
203
205
|
*
|
|
204
206
|
* @example
|
|
205
207
|
* ```typescript
|
|
@@ -229,7 +231,10 @@ export var userAppData;
|
|
|
229
231
|
export var appConfiguration;
|
|
230
232
|
(function (appConfiguration) {
|
|
231
233
|
/**
|
|
232
|
-
|
|
234
|
+
* Get app configuration for a collection/product scope.
|
|
235
|
+
* Public reads return the public view of the config. If the stored config contains a
|
|
236
|
+
* top-level `admin` object, that block is omitted from public responses and included
|
|
237
|
+
* when `opts.admin === true`.
|
|
233
238
|
*
|
|
234
239
|
* @param opts - Configuration options including appId and scope (collectionId, productId, etc.)
|
|
235
240
|
* @returns The configuration object
|
|
@@ -309,8 +314,10 @@ export var appConfiguration;
|
|
|
309
314
|
}
|
|
310
315
|
appConfiguration.listWidgetInstances = listWidgetInstances;
|
|
311
316
|
/**
|
|
312
|
-
|
|
313
|
-
|
|
317
|
+
* Set app configuration for a collection/product scope.
|
|
318
|
+
* Requires admin authentication.
|
|
319
|
+
* Writing through the admin endpoint does not make every root-level field private.
|
|
320
|
+
* Use `config.admin` for confidential values that should only be returned on admin reads.
|
|
314
321
|
*
|
|
315
322
|
* @param opts - Configuration options including appId, scope, and config data
|
|
316
323
|
* @returns The saved configuration
|
package/dist/api/collection.d.ts
CHANGED
|
@@ -22,9 +22,13 @@ export declare namespace collection {
|
|
|
22
22
|
*/
|
|
23
23
|
function getShortId(shortId: string): Promise<CollectionResponse>;
|
|
24
24
|
/**
|
|
25
|
-
* Retrieve a specific settings group for a collection
|
|
25
|
+
* Retrieve a specific settings group for a collection.
|
|
26
|
+
* Public reads return the public view of the settings group. If the stored payload contains
|
|
27
|
+
* a top-level `admin` object, that block is omitted from public responses and included when
|
|
28
|
+
* `admin === true`.
|
|
26
29
|
* @param collectionId – Identifier of the collection
|
|
27
30
|
* @param settingGroup – The settings group name
|
|
31
|
+
* @param admin – If true, use the admin endpoint and include the admin-only settings block
|
|
28
32
|
* @returns Promise resolving to the settings object
|
|
29
33
|
*/
|
|
30
34
|
function getSettings(collectionId: string, settingGroup: string, admin?: boolean): Promise<any>;
|
|
@@ -37,6 +41,9 @@ export declare namespace collection {
|
|
|
37
41
|
function getAppsConfig(collectionId: string): Promise<AppsConfigResponse>;
|
|
38
42
|
/**
|
|
39
43
|
* Update a specific settings group for a collection (admin endpoint).
|
|
44
|
+
* This writes through the admin endpoint, but root-level fields are still part of the public
|
|
45
|
+
* settings payload. Put confidential values under `settings.admin` if they should only be
|
|
46
|
+
* returned on admin reads.
|
|
40
47
|
* @param collectionId – Identifier of the collection
|
|
41
48
|
* @param settingGroup – The settings group name
|
|
42
49
|
* @param settings – The settings payload to persist
|
package/dist/api/collection.js
CHANGED
|
@@ -38,9 +38,13 @@ export var collection;
|
|
|
38
38
|
}
|
|
39
39
|
collection.getShortId = getShortId;
|
|
40
40
|
/**
|
|
41
|
-
* Retrieve a specific settings group for a collection
|
|
41
|
+
* Retrieve a specific settings group for a collection.
|
|
42
|
+
* Public reads return the public view of the settings group. If the stored payload contains
|
|
43
|
+
* a top-level `admin` object, that block is omitted from public responses and included when
|
|
44
|
+
* `admin === true`.
|
|
42
45
|
* @param collectionId – Identifier of the collection
|
|
43
46
|
* @param settingGroup – The settings group name
|
|
47
|
+
* @param admin – If true, use the admin endpoint and include the admin-only settings block
|
|
44
48
|
* @returns Promise resolving to the settings object
|
|
45
49
|
*/
|
|
46
50
|
async function getSettings(collectionId, settingGroup, admin) {
|
|
@@ -62,6 +66,9 @@ export var collection;
|
|
|
62
66
|
collection.getAppsConfig = getAppsConfig;
|
|
63
67
|
/**
|
|
64
68
|
* Update a specific settings group for a collection (admin endpoint).
|
|
69
|
+
* This writes through the admin endpoint, but root-level fields are still part of the public
|
|
70
|
+
* settings payload. Put confidential values under `settings.admin` if they should only be
|
|
71
|
+
* returned on admin reads.
|
|
65
72
|
* @param collectionId – Identifier of the collection
|
|
66
73
|
* @param settingGroup – The settings group name
|
|
67
74
|
* @param settings – The settings payload to persist
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.9 | Generated: 2026-03-19T15:02:36.521Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -42,6 +42,16 @@ When you need flexible app-specific data, choose the storage model based on shap
|
|
|
42
42
|
|
|
43
43
|
Rule of thumb: if you are modelling a real domain object that users will browse, filter, secure, or evolve over time, start with app objects. If you just need a simple keyed payload hanging off a collection or product, scoped data items are still a good fit.
|
|
44
44
|
|
|
45
|
+
## Settings Visibility
|
|
46
|
+
|
|
47
|
+
For `appConfiguration` config blobs and `collection` settings groups, keep endpoint choice separate from visibility semantics.
|
|
48
|
+
|
|
49
|
+
- **`admin: true` means "use the admin endpoint"** for reads or writes.
|
|
50
|
+
- **It does not make every root field private.** Writing through an admin endpoint still saves the normal shared payload.
|
|
51
|
+
- **Root-level fields are the public/shared settings view.** Put public labels, colors, toggles, and general config there.
|
|
52
|
+
- **Use a top-level `admin` object for confidential values.** Public reads omit that block; admin reads include it.
|
|
53
|
+
- **Applies to both** `appConfiguration.getConfig` / `setConfig` **and** `collection.getSettings` / `updateSettings`.
|
|
54
|
+
|
|
45
55
|
## API Namespaces
|
|
46
56
|
|
|
47
57
|
The Smartlinks SDK is organized into the following namespaces:
|
|
@@ -5834,10 +5844,17 @@ type AppConfigOptions = {
|
|
|
5834
5844
|
/** Item ID - required for getDataItem/deleteDataItem */
|
|
5835
5845
|
itemId?: string
|
|
5836
5846
|
|
|
5837
|
-
/**
|
|
5847
|
+
/**
|
|
5848
|
+
* Use admin endpoints instead of public.
|
|
5849
|
+
* This selects which endpoint is called; it does not by itself make root-level config fields private.
|
|
5850
|
+
*/
|
|
5838
5851
|
admin?: boolean
|
|
5839
5852
|
|
|
5840
|
-
/**
|
|
5853
|
+
/**
|
|
5854
|
+
* Configuration object for setConfig.
|
|
5855
|
+
* For admin-only values in app config, store them under a top-level `admin` object.
|
|
5856
|
+
* Public reads return the root config but omit `config.admin`.
|
|
5857
|
+
*/
|
|
5841
5858
|
config?: any
|
|
5842
5859
|
/** Data object for setDataItem. Best for small keyed scoped documents rather than richer app domain objects. */
|
|
5843
5860
|
data?: any
|
|
@@ -6130,7 +6147,7 @@ Get aggregate statistics for threads POST /threads/aggregate
|
|
|
6130
6147
|
Scoped config and keyed data items for collections, products, variants, or batches. Best for settings and small standalone documents, not as the default answer for every app-owned entity.
|
|
6131
6148
|
|
|
6132
6149
|
**getConfig**(opts: AppConfigOptions) → `Promise<any>`
|
|
6133
|
-
Get app configuration for a collection/product scope. ```typescript const config = await appConfiguration.getConfig({ appId: 'warranty-portal', collectionId: 'my-collection' }); ```
|
|
6150
|
+
Get app configuration for a collection/product scope. Public reads return the public view of the config. If the stored config contains a top-level `admin` object, that block is omitted from public responses and included when `opts.admin === true`. ```typescript const config = await appConfiguration.getConfig({ appId: 'warranty-portal', collectionId: 'my-collection' }); ```
|
|
6134
6151
|
|
|
6135
6152
|
**getWidgetInstance**(opts: GetWidgetInstanceOptions) → `Promise<WidgetInstance<TWidget>>`
|
|
6136
6153
|
Resolve a configured widget instance by ID from an app's stored config. This is a thin convenience wrapper over `getConfig()` that reads `config.widgets[widgetId]`. ```typescript const widget = await appConfiguration.getWidgetInstance({ collectionId: 'my-collection', appId: 'widget-toolkit', widgetId: 'launch-countdown' }) ```
|
|
@@ -6139,7 +6156,7 @@ Resolve a configured widget instance by ID from an app's stored config. This is
|
|
|
6139
6156
|
List configured widget instances for an app. Useful for picker UIs, setup schemas, and widget-to-widget references. ```typescript const widgets = await appConfiguration.listWidgetInstances({ collectionId: 'my-collection', appId: 'widget-toolkit' }) ```
|
|
6140
6157
|
|
|
6141
6158
|
**setConfig**(opts: AppConfigOptions) → `Promise<any>`
|
|
6142
|
-
Set app configuration for a collection/product scope. Requires admin authentication. ```typescript await appConfiguration.setConfig({ appId: 'warranty-portal', collectionId: 'my-collection', admin: true, config: { warrantyPeriod: 24, supportEmail: 'support@example.com' } }); ```
|
|
6159
|
+
Set app configuration for a collection/product scope. Requires admin authentication. Writing through the admin endpoint does not make every root-level field private. Use `config.admin` for confidential values that should only be returned on admin reads. ```typescript await appConfiguration.setConfig({ appId: 'warranty-portal', collectionId: 'my-collection', admin: true, config: { warrantyPeriod: 24, supportEmail: 'support@example.com' } }); ```
|
|
6143
6160
|
|
|
6144
6161
|
**deleteConfig**(opts: AppConfigOptions) → `Promise<void>`
|
|
6145
6162
|
Delete app configuration for a collection/product scope. Requires admin authentication. ```typescript await appConfiguration.deleteConfig({ appId: 'warranty-portal', collectionId: 'my-collection', admin: true }); ```
|
|
@@ -6614,13 +6631,13 @@ Retrieves all Collections.
|
|
|
6614
6631
|
Retrieve a collection by its shortId (public endpoint).
|
|
6615
6632
|
|
|
6616
6633
|
**getSettings**(collectionId: string, settingGroup: string, admin?: boolean) → `Promise<any>`
|
|
6617
|
-
Retrieve a specific settings group for a collection
|
|
6634
|
+
Retrieve a specific settings group for a collection. Public reads return the public view of the settings group. If the stored payload contains a top-level `admin` object, that block is omitted from public responses and included when `admin === true`.
|
|
6618
6635
|
|
|
6619
6636
|
**getAppsConfig**(collectionId: string) → `Promise<AppsConfigResponse>`
|
|
6620
6637
|
Retrieve all configured app module definitions for a collection (public endpoint).
|
|
6621
6638
|
|
|
6622
6639
|
**updateSettings**(collectionId: string, settingGroup: string, settings: any) → `Promise<any>`
|
|
6623
|
-
Update a specific settings group for a collection (admin endpoint).
|
|
6640
|
+
Update a specific settings group for a collection (admin endpoint). This writes through the admin endpoint, but root-level fields are still part of the public settings payload. Put confidential values under `settings.admin` if they should only be returned on admin reads.
|
|
6624
6641
|
|
|
6625
6642
|
**create**(data: CollectionCreateRequest) → `Promise<CollectionResponse>`
|
|
6626
6643
|
Create a new collection (admin only).
|
|
@@ -199,6 +199,26 @@ await SL.appConfiguration.setConfig({
|
|
|
199
199
|
});
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
Important visibility rule for app settings:
|
|
203
|
+
|
|
204
|
+
- `admin: true` means the SDK uses the admin endpoint.
|
|
205
|
+
- It does not make every root field private.
|
|
206
|
+
- If a value should only be visible to admins, put it under `config.admin`.
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
await SL.appConfiguration.setConfig({
|
|
210
|
+
collectionId,
|
|
211
|
+
appId,
|
|
212
|
+
admin: true,
|
|
213
|
+
config: {
|
|
214
|
+
enableNotifications: true,
|
|
215
|
+
admin: {
|
|
216
|
+
apiToken: 'secret-token'
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
202
222
|
---
|
|
203
223
|
|
|
204
224
|
## Metrics & Analytics
|
|
@@ -228,6 +248,7 @@ await SL.appConfiguration.setConfig({
|
|
|
228
248
|
| Issue | Cause | Fix |
|
|
229
249
|
| --------------------- | -------------------------- | ----------------------------------------------------- |
|
|
230
250
|
| Config save fails | Missing `admin: true` flag | Always include `admin: true` for admin operations |
|
|
251
|
+
| Secret visible publicly | Saved secret at root level | Put confidential values under the top-level `admin` object |
|
|
231
252
|
| Widget doesn't render | Missing required props | Ensure `collectionId`, `appId`, and `SL` are provided |
|
|
232
253
|
| Import skips rows | Invalid `productId` | Verify product IDs exist in the collection |
|
|
233
254
|
| Theme not applied | Missing `?theme=` param | Check URL parameters or postMessage setup |
|
|
@@ -104,6 +104,49 @@ This data is scoped to specific collections, products, variants, or batches. It'
|
|
|
104
104
|
|
|
105
105
|
If you need richer app-owned entities with querying, lifecycle, access zones, parent-child relationships, or workflow semantics, use [app-objects.md](app-objects.md) instead of forcing everything through `setDataItem`.
|
|
106
106
|
|
|
107
|
+
### Critical: `admin: true` Controls the Endpoint, Not Field Privacy
|
|
108
|
+
|
|
109
|
+
This is the part that often gets misunderstood by apps and AI tools:
|
|
110
|
+
|
|
111
|
+
- `admin: true` means "call the admin endpoint".
|
|
112
|
+
- It does **not** mean "everything I wrote is now admin-only".
|
|
113
|
+
- For app config and collection settings, root-level fields are usually part of the public payload.
|
|
114
|
+
- If you need confidential values, store them under a top-level `admin` object.
|
|
115
|
+
- Public reads omit that `admin` block; admin reads include it.
|
|
116
|
+
|
|
117
|
+
Example for app config:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
await appConfiguration.setConfig({
|
|
121
|
+
appId: 'warranty-portal',
|
|
122
|
+
collectionId: 'my-collection',
|
|
123
|
+
admin: true,
|
|
124
|
+
config: {
|
|
125
|
+
theme: 'gold',
|
|
126
|
+
supportEmail: 'support@example.com',
|
|
127
|
+
admin: {
|
|
128
|
+
accessToken: 'secret-token',
|
|
129
|
+
webhookSecret: 'top-secret'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const publicView = await appConfiguration.getConfig({
|
|
135
|
+
appId: 'warranty-portal',
|
|
136
|
+
collectionId: 'my-collection'
|
|
137
|
+
})
|
|
138
|
+
// { theme: 'gold', supportEmail: 'support@example.com' }
|
|
139
|
+
|
|
140
|
+
const adminView = await appConfiguration.getConfig({
|
|
141
|
+
appId: 'warranty-portal',
|
|
142
|
+
collectionId: 'my-collection',
|
|
143
|
+
admin: true
|
|
144
|
+
})
|
|
145
|
+
// { theme: 'gold', supportEmail: 'support@example.com', admin: { ... } }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The same visibility pattern applies to collection settings via `collection.getSettings()` / `collection.updateSettings()`.
|
|
149
|
+
|
|
107
150
|
### Use Cases
|
|
108
151
|
- App-specific settings for a collection
|
|
109
152
|
- Product-level configuration
|
|
@@ -178,7 +221,7 @@ await appConfiguration.setDataItem({
|
|
|
178
221
|
| **Shared across collections?** | ✅ Yes | ❌ No | ❌ No |
|
|
179
222
|
| **Requires auth?** | ✅ Yes (user token) | ✅ Yes (admin token for write) | Depends on public/admin endpoint and visibility |
|
|
180
223
|
| **Querying / filtering** | Minimal | Minimal | Strong |
|
|
181
|
-
| **Access control zones** | User-scoped only |
|
|
224
|
+
| **Access control zones** | User-scoped only | Root fields + reserved `admin` block for settings/config | `data` / `owner` / `admin` zones |
|
|
182
225
|
| **Admin write required?** | ❌ No | ✅ Yes (for write operations) | Only for admin endpoints / admin fields |
|
|
183
226
|
|
|
184
227
|
---
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Building React Components for SmartLinks
|
|
2
|
+
|
|
3
|
+
> **Read this first** before implementing widgets or containers for the SmartLinks platform.
|
|
4
|
+
|
|
5
|
+
This guide covers the fundamental concepts you need to understand to build React components that work correctly in the SmartLinks ecosystem. For implementation details, see [widgets.md](./widgets.md) and [containers.md](./containers.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
1. [Dual-Mode Rendering](#dual-mode-rendering)
|
|
12
|
+
2. [The Router Contract](#the-router-contract)
|
|
13
|
+
3. [The useAppContext Pattern](#the-useappcontext-pattern)
|
|
14
|
+
4. [Common Pitfalls](#common-pitfalls)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Dual-Mode Rendering
|
|
19
|
+
|
|
20
|
+
Every SmartLinks component can run in **two modes**. The platform decides which mode to use based on configuration — your app doesn't choose.
|
|
21
|
+
|
|
22
|
+
| Mode | How It Works | When Used |
|
|
23
|
+
|------|-------------|-----------|
|
|
24
|
+
| **Direct Component** | Your component runs directly in the parent's React context | Default - better performance, shared context |
|
|
25
|
+
| **Iframe** | Your app runs inside an iframe with its own URL | Fallback - full isolation when needed |
|
|
26
|
+
|
|
27
|
+
**Key insight:** You write your component code once, and it must work correctly in **both** modes.
|
|
28
|
+
|
|
29
|
+
### What Changes Between Modes?
|
|
30
|
+
|
|
31
|
+
| Aspect | Direct Component Mode | Iframe Mode |
|
|
32
|
+
|--------|----------------------|-------------|
|
|
33
|
+
| **Context source** | Props passed from parent | URL search parameters |
|
|
34
|
+
| **Router** | Parent provides `MemoryRouter` | Your app provides `HashRouter` |
|
|
35
|
+
| **Styling** | Inherits parent's CSS scope | Fully isolated CSS |
|
|
36
|
+
| **SDK instance** | Shared with parent via props | Own instance from `window.SL` |
|
|
37
|
+
| **Communication** | Direct callbacks (props) | `postMessage` |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## The Router Contract
|
|
42
|
+
|
|
43
|
+
This is the **most important rule** to avoid runtime errors:
|
|
44
|
+
|
|
45
|
+
### ❌ The Critical Rule
|
|
46
|
+
|
|
47
|
+
> **Your exported component (`PublicContainer` or `PublicComponent`) must NOT be wrapped in any `<Router>` component.**
|
|
48
|
+
|
|
49
|
+
If you wrap your export in `<MemoryRouter>`, `<HashRouter>`, or `<BrowserRouter>`, React Router will throw:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Error: You cannot render a <Router> inside another <Router>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### ✅ Where Routing Goes
|
|
56
|
+
|
|
57
|
+
**For Widgets (no routing needed):**
|
|
58
|
+
```tsx
|
|
59
|
+
// src/exports/PublicComponent.tsx
|
|
60
|
+
export const PublicComponent = (props) => {
|
|
61
|
+
return <WidgetContent />; // No router needed
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**For Containers (with internal navigation):**
|
|
66
|
+
```tsx
|
|
67
|
+
// src/exports/PublicContainer.tsx
|
|
68
|
+
import { Routes, Route } from 'react-router-dom';
|
|
69
|
+
|
|
70
|
+
export const PublicContainer = (props) => {
|
|
71
|
+
return (
|
|
72
|
+
<Routes> {/* ✅ Routes are fine */}
|
|
73
|
+
<Route path="/" element={<Home />} />
|
|
74
|
+
<Route path="/detail/:id" element={<Detail />} />
|
|
75
|
+
</Routes>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**For Iframe Entry Point:**
|
|
81
|
+
```tsx
|
|
82
|
+
// src/App.tsx (only used in iframe mode)
|
|
83
|
+
import { HashRouter, Routes, Route } from 'react-router-dom';
|
|
84
|
+
|
|
85
|
+
function App() {
|
|
86
|
+
return (
|
|
87
|
+
<HashRouter> {/* ✅ HashRouter only in iframe entry point */}
|
|
88
|
+
<Routes>
|
|
89
|
+
<Route path="/" element={<Home />} />
|
|
90
|
+
</Routes>
|
|
91
|
+
</HashRouter>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Why This Matters
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
Direct Component Mode:
|
|
100
|
+
Portal App
|
|
101
|
+
└─ MemoryRouter ← Parent provides this
|
|
102
|
+
└─ Your Component
|
|
103
|
+
└─ [If you add another Router here, it breaks]
|
|
104
|
+
|
|
105
|
+
Iframe Mode:
|
|
106
|
+
<iframe>
|
|
107
|
+
└─ Your App.tsx
|
|
108
|
+
└─ HashRouter ← You provide this
|
|
109
|
+
└─ Your Component
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## The useAppContext Pattern
|
|
115
|
+
|
|
116
|
+
To access context (collectionId, productId, etc.) in a way that works in **both** modes, use this abstraction:
|
|
117
|
+
|
|
118
|
+
### The Hook
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
// src/hooks/useAppContext.ts
|
|
122
|
+
import { useContext, createContext, useMemo } from 'react';
|
|
123
|
+
import { useSearchParams } from 'react-router-dom';
|
|
124
|
+
|
|
125
|
+
export interface AppContextValue {
|
|
126
|
+
collectionId: string;
|
|
127
|
+
appId: string;
|
|
128
|
+
productId?: string;
|
|
129
|
+
proofId?: string;
|
|
130
|
+
pageId?: string;
|
|
131
|
+
lang?: string;
|
|
132
|
+
user?: { id: string; email: string; name?: string };
|
|
133
|
+
SL: typeof import('@proveanything/smartlinks');
|
|
134
|
+
onNavigate?: (request: any) => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const AppContext = createContext<AppContextValue | null>(null);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns app context regardless of rendering mode.
|
|
141
|
+
* - Direct component mode: reads from React Context (props)
|
|
142
|
+
* - Iframe mode: reads from URL search params
|
|
143
|
+
*/
|
|
144
|
+
export function useAppContext(): AppContextValue {
|
|
145
|
+
const ctx = useContext(AppContext);
|
|
146
|
+
|
|
147
|
+
// Direct-component mode
|
|
148
|
+
if (ctx) return ctx;
|
|
149
|
+
|
|
150
|
+
// Iframe mode — read from URL
|
|
151
|
+
const [searchParams] = useSearchParams();
|
|
152
|
+
const SL = (window as any).SL ?? require('@proveanything/smartlinks');
|
|
153
|
+
|
|
154
|
+
return useMemo(() => ({
|
|
155
|
+
collectionId: searchParams.get('collectionId') ?? '',
|
|
156
|
+
appId: searchParams.get('appId') ?? '',
|
|
157
|
+
productId: searchParams.get('productId') ?? undefined,
|
|
158
|
+
proofId: searchParams.get('proofId') ?? undefined,
|
|
159
|
+
pageId: searchParams.get('pageId') ?? undefined,
|
|
160
|
+
lang: searchParams.get('lang') ?? undefined,
|
|
161
|
+
SL,
|
|
162
|
+
}), [searchParams, SL]);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Wire It Up in Your Export
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// src/exports/PublicContainer.tsx
|
|
170
|
+
import { AppContext } from '@/hooks/useAppContext';
|
|
171
|
+
|
|
172
|
+
export const PublicContainer = (props: Record<string, any>) => {
|
|
173
|
+
return (
|
|
174
|
+
<AppContext.Provider value={props}>
|
|
175
|
+
<YourComponentTree />
|
|
176
|
+
</AppContext.Provider>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Use It in Your Components
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
// Anywhere in your component tree
|
|
185
|
+
import { useAppContext } from '@/hooks/useAppContext';
|
|
186
|
+
|
|
187
|
+
function MyComponent() {
|
|
188
|
+
const { collectionId, productId, SL } = useAppContext();
|
|
189
|
+
// This works in both direct-component and iframe modes!
|
|
190
|
+
|
|
191
|
+
return <div>Collection: {collectionId}</div>;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Why Not Just useSearchParams()?
|
|
196
|
+
|
|
197
|
+
In **direct-component mode**, there are no URL search params — context comes as props. If you use `useSearchParams()` directly, it will return empty values. The `useAppContext()` pattern handles both cases automatically.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Common Pitfalls
|
|
202
|
+
|
|
203
|
+
### ❌ "You cannot render a `<Router>` inside another `<Router>`"
|
|
204
|
+
|
|
205
|
+
**Cause:** Your exported component wraps itself in a Router.
|
|
206
|
+
|
|
207
|
+
**Fix:** Remove the Router wrapper from `PublicContainer.tsx` or `PublicComponent.tsx`. Only your iframe entry point (`App.tsx`) should have a Router.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### ❌ "Invalid hook call" / Minified React error #321
|
|
212
|
+
|
|
213
|
+
**Cause:** Your bundle includes its own copy of React instead of using the parent's shared instance.
|
|
214
|
+
|
|
215
|
+
**Fix:** Ensure your build configuration externalizes React, ReactDOM, and react/jsx-runtime:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// vite.config.container.ts or vite.config.widget.ts
|
|
219
|
+
export default defineConfig({
|
|
220
|
+
build: {
|
|
221
|
+
rollupOptions: {
|
|
222
|
+
external: [
|
|
223
|
+
'react',
|
|
224
|
+
'react-dom',
|
|
225
|
+
'react/jsx-runtime',
|
|
226
|
+
'react-router-dom',
|
|
227
|
+
'@proveanything/smartlinks',
|
|
228
|
+
// ... other shared dependencies
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### ❌ Context values are undefined in direct-component mode
|
|
238
|
+
|
|
239
|
+
**Cause:** Using `useSearchParams()` or reading from `window.location` instead of using the `useAppContext()` pattern.
|
|
240
|
+
|
|
241
|
+
**Fix:** Implement the `useAppContext()` hook as shown above and use it throughout your component tree.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### ❌ Navigation doesn't work
|
|
246
|
+
|
|
247
|
+
**Cause:** Using `window.location` or `window.parent.postMessage` for navigation.
|
|
248
|
+
|
|
249
|
+
**Fix:** Use the `onNavigate` callback from props/context with structured `NavigationRequest` objects. See [widgets.md](./widgets.md#cross-app-navigation) for details.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### ❌ Styles leak between apps
|
|
254
|
+
|
|
255
|
+
**Cause:** Direct components share the parent's CSS scope — there's no iframe isolation.
|
|
256
|
+
|
|
257
|
+
**Fix:** Use CSS Modules, scoped class prefixes, or Tailwind with a unique prefix. Avoid global CSS selectors in your bundle.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### ❌ CORS errors when loading bundles
|
|
262
|
+
|
|
263
|
+
**Cause:** Your widget/container JavaScript or CSS is served from a different origin without CORS headers.
|
|
264
|
+
|
|
265
|
+
**Fix:** Configure CORS headers on your hosting, or use inline bundle source via the SDK's `getWidgets()` API.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Quick Diagnostic Checklist
|
|
270
|
+
|
|
271
|
+
| Issue | Check |
|
|
272
|
+
|-------|-------|
|
|
273
|
+
| Router error | Remove all `<Router>` wrappers from your exported component |
|
|
274
|
+
| Invalid hook call | Verify React is externalized in your build config |
|
|
275
|
+
| Missing context | Use `useAppContext()` instead of `useSearchParams()` |
|
|
276
|
+
| Duplicate React | Check `window.React === yourBundle.React` in browser console |
|
|
277
|
+
| CSS conflicts | Use scoped CSS (modules/prefixes), avoid global selectors |
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## File Structure Reference
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
my-app/
|
|
285
|
+
├── src/
|
|
286
|
+
│ ├── App.tsx ← Iframe entry (has HashRouter)
|
|
287
|
+
│ ├── exports/
|
|
288
|
+
│ │ ├── PublicContainer.tsx ← Container export (no Router!)
|
|
289
|
+
│ │ └── PublicComponent.tsx ← Widget export (no Router!)
|
|
290
|
+
│ ├── hooks/
|
|
291
|
+
│ │ └── useAppContext.ts ← Dual-mode abstraction
|
|
292
|
+
│ ├── pages/
|
|
293
|
+
│ │ ├── Home.tsx ← Shared components
|
|
294
|
+
│ │ └── Detail.tsx
|
|
295
|
+
│ └── main.tsx ← Iframe bootstrap
|
|
296
|
+
├── vite.config.ts ← Iframe build
|
|
297
|
+
├── vite.config.container.ts ← Container bundle
|
|
298
|
+
└── vite.config.widget.ts ← Widget bundle
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Next Steps
|
|
304
|
+
|
|
305
|
+
Now that you understand the core concepts, see the implementation guides:
|
|
306
|
+
|
|
307
|
+
- **[widgets.md](./widgets.md)** — Build lightweight preview components
|
|
308
|
+
- **[containers.md](./containers.md)** — Build full-page embedded experiences
|
|
309
|
+
- **[mpa.md](./mpa.md)** — Multi-page app architecture (optional)
|
|
310
|
+
- **[iframe-responder.md](./iframe-responder.md)** — Parent-side iframe integration
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Related Documentation
|
|
315
|
+
|
|
316
|
+
- [theme.system.md](./theme.system.md) — Theming and CSS variables
|
|
317
|
+
- [ai.md](./ai.md) — AI assistant integration
|
|
318
|
+
- [deep-link-discovery.md](./deep-link-discovery.md) — Deep linking patterns
|
|
319
|
+
- [manifests.md](./manifests.md) — App manifest configuration
|