@proveanything/smartlinks 1.4.5 → 1.4.7
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/docs/API_SUMMARY.md +34 -21
- package/dist/docs/containers.md +68 -2
- package/dist/docs/widgets.md +113 -45
- package/dist/types/appManifest.d.ts +54 -31
- package/docs/API_SUMMARY.md +34 -21
- package/docs/containers.md +68 -2
- package/docs/widgets.md +113 -45
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.7 | Generated: 2026-02-21T14:49:14.374Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -869,28 +869,14 @@ interface AppContainerComponent {
|
|
|
869
869
|
}
|
|
870
870
|
```
|
|
871
871
|
|
|
872
|
-
**
|
|
872
|
+
**AppAdminConfig** (interface)
|
|
873
873
|
```typescript
|
|
874
|
-
interface
|
|
874
|
+
interface AppAdminConfig {
|
|
875
875
|
$schema?: string;
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
platformRevision?: string;
|
|
881
|
-
appId: string;
|
|
882
|
-
};
|
|
883
|
-
* Filename of the admin configuration JSON (e.g. "app.admin.json").
|
|
884
|
-
* Present when the app ships an admin UI config.
|
|
885
|
-
admin?: string;
|
|
886
|
-
widgets?: {
|
|
887
|
-
files: AppManifestFiles;
|
|
888
|
-
components: AppWidgetComponent[];
|
|
889
|
-
};
|
|
890
|
-
containers?: {
|
|
891
|
-
files: AppManifestFiles;
|
|
892
|
-
components: AppContainerComponent[];
|
|
893
|
-
};
|
|
876
|
+
* Path (relative to the app's public root) to an AI guide markdown file.
|
|
877
|
+
* Provides natural-language context for AI-assisted configuration.
|
|
878
|
+
* @example "ai-guide.md"
|
|
879
|
+
aiGuide?: string;
|
|
894
880
|
setup?: {
|
|
895
881
|
description?: string;
|
|
896
882
|
questions?: Array<{
|
|
@@ -944,6 +930,33 @@ interface AppManifest {
|
|
|
944
930
|
interactions?: Array<{ id: string; description?: string }>;
|
|
945
931
|
kpis?: Array<{ name: string; compute?: string }>;
|
|
946
932
|
};
|
|
933
|
+
}
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
**AppManifest** (interface)
|
|
937
|
+
```typescript
|
|
938
|
+
interface AppManifest {
|
|
939
|
+
$schema?: string;
|
|
940
|
+
meta?: {
|
|
941
|
+
name: string;
|
|
942
|
+
description?: string;
|
|
943
|
+
version: string;
|
|
944
|
+
platformRevision?: string;
|
|
945
|
+
appId: string;
|
|
946
|
+
};
|
|
947
|
+
* Relative path to the admin configuration file (e.g. `"app.admin.json"`).
|
|
948
|
+
* When present, fetch this file to get the full {@link AppAdminConfig}
|
|
949
|
+
* (setup questions, import schema, tunable fields, metrics definitions).
|
|
950
|
+
* Absent when the app has no admin UI.
|
|
951
|
+
admin?: string;
|
|
952
|
+
widgets?: {
|
|
953
|
+
files: AppManifestFiles;
|
|
954
|
+
components: AppWidgetComponent[];
|
|
955
|
+
};
|
|
956
|
+
containers?: {
|
|
957
|
+
files: AppManifestFiles;
|
|
958
|
+
components: AppContainerComponent[];
|
|
959
|
+
};
|
|
947
960
|
[key: string]: any;
|
|
948
961
|
}
|
|
949
962
|
```
|
package/dist/docs/containers.md
CHANGED
|
@@ -47,7 +47,7 @@ All standard widget props apply:
|
|
|
47
47
|
| `productId` | string | ❌ | Product context |
|
|
48
48
|
| `proofId` | string | ❌ | Proof context |
|
|
49
49
|
| `user` | object | ❌ | Current user info |
|
|
50
|
-
| `onNavigate` | function | ❌ | Navigation callback
|
|
50
|
+
| `onNavigate` | function | ❌ | Navigation callback (accepts `NavigationRequest` or legacy string) |
|
|
51
51
|
| `size` | string | ❌ | `"compact"`, `"standard"`, or `"large"` |
|
|
52
52
|
| `lang` | string | ❌ | Language code (e.g., `"en"`) |
|
|
53
53
|
| `translations` | object | ❌ | Translation overrides |
|
|
@@ -55,6 +55,67 @@ All standard widget props apply:
|
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
58
|
+
## Cross-App Navigation
|
|
59
|
+
|
|
60
|
+
Containers support the same **structured navigation requests** as widgets. When a container needs to navigate to another app within the portal, it emits a `NavigationRequest` via the `onNavigate` callback. The portal orchestrator interprets the request, preserves hierarchy context, and performs the navigation.
|
|
61
|
+
|
|
62
|
+
### NavigationRequest
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
interface NavigationRequest {
|
|
66
|
+
/** Target app ID to activate */
|
|
67
|
+
appId: string;
|
|
68
|
+
/** Deep link / page within the target app (forwarded as pageId) */
|
|
69
|
+
deepLink?: string;
|
|
70
|
+
/** Extra params forwarded to the target app */
|
|
71
|
+
params?: Record<string, string>;
|
|
72
|
+
/** Optionally switch to a specific product before showing the app */
|
|
73
|
+
productId?: string;
|
|
74
|
+
/** Optionally switch to a specific proof before showing the app */
|
|
75
|
+
proofId?: string;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Usage in Containers
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Inside a container component
|
|
83
|
+
const handleNavigateToWarranty = () => {
|
|
84
|
+
onNavigate?.({
|
|
85
|
+
appId: 'warranty-app',
|
|
86
|
+
deepLink: 'register',
|
|
87
|
+
params: { source: 'product-page' },
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Switch product context and open an app
|
|
92
|
+
const handleViewRelatedProduct = (productId: string) => {
|
|
93
|
+
onNavigate?.({
|
|
94
|
+
appId: 'product-info',
|
|
95
|
+
productId,
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### How It Works
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
Container button "Register Warranty"
|
|
104
|
+
→ onNavigate({ appId: 'warranty', deepLink: 'register', params: { ref: 'container' } })
|
|
105
|
+
→ Portal orchestrator receives NavigationRequest
|
|
106
|
+
→ Calls actions.navigateToApp('warranty', 'register')
|
|
107
|
+
→ Target app loads with extraParams: { pageId: 'register', ref: 'container' }
|
|
108
|
+
→ collectionId, productId, proofId, theme all preserved automatically
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Backward Compatibility
|
|
112
|
+
|
|
113
|
+
The `onNavigate` callback accepts both structured `NavigationRequest` objects and legacy strings. Existing containers that call `onNavigate('/some-path')` continue to work. **New containers should always use the structured `NavigationRequest` format.**
|
|
114
|
+
|
|
115
|
+
See `widgets.md` for the full `NavigationRequest` documentation and additional examples.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
58
119
|
## Architecture
|
|
59
120
|
|
|
60
121
|
Containers use **MemoryRouter** (not HashRouter) because the parent app owns the browser's URL bar. Context is passed via props rather than URL parameters. Each container gets its own `QueryClient` to avoid cache collisions with the parent app.
|
|
@@ -83,7 +144,10 @@ const { PublicContainer } = await import('https://my-app.com/containers.es.js');
|
|
|
83
144
|
appId="my-app"
|
|
84
145
|
productId="prod-123"
|
|
85
146
|
SL={SL}
|
|
86
|
-
onNavigate={
|
|
147
|
+
onNavigate={(request) => {
|
|
148
|
+
// The portal orchestrator handles NavigationRequest objects
|
|
149
|
+
// automatically when using ContentOrchestrator / OrchestratedPortal
|
|
150
|
+
}}
|
|
87
151
|
lang="en"
|
|
88
152
|
className="max-w-4xl mx-auto"
|
|
89
153
|
/>
|
|
@@ -184,6 +248,7 @@ src/containers/
|
|
|
184
248
|
| **Styling** | Fully isolated CSS | Inherits parent CSS variables |
|
|
185
249
|
| **Communication** | postMessage | Direct props / callbacks |
|
|
186
250
|
| **Auth** | Via `SL.auth.getAccount()` | Via `user` prop from parent |
|
|
251
|
+
| **Navigation** | `window.parent.postMessage` | `onNavigate` with `NavigationRequest` |
|
|
187
252
|
|
|
188
253
|
---
|
|
189
254
|
|
|
@@ -196,3 +261,4 @@ src/containers/
|
|
|
196
261
|
| Routing doesn't work | Using HashRouter instead of MemoryRouter | Containers must use MemoryRouter |
|
|
197
262
|
| Query cache conflicts | Sharing parent's QueryClient | Each container needs its own `QueryClient` instance |
|
|
198
263
|
| `cva.cva` runtime error | Global set to lowercase `cva` | Use uppercase `CVA` for the global name |
|
|
264
|
+
| Navigation does nothing | Using legacy string with `onNavigate` | Use structured `NavigationRequest` object instead |
|
package/dist/docs/widgets.md
CHANGED
|
@@ -10,7 +10,7 @@ Widgets are self-contained React components that:
|
|
|
10
10
|
- Run inside the parent React application (not iframes)
|
|
11
11
|
- Receive standardized context props
|
|
12
12
|
- Inherit styling from the parent via CSS variables
|
|
13
|
-
- Can trigger navigation
|
|
13
|
+
- Can trigger structured cross-app navigation within the parent portal
|
|
14
14
|
|
|
15
15
|
```text
|
|
16
16
|
┌─────────────────────────────────────────────────────────────────┐
|
|
@@ -18,8 +18,8 @@ Widgets are self-contained React components that:
|
|
|
18
18
|
│ │
|
|
19
19
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
20
20
|
│ │ Competition │ │ Music App │ │ Warranty │ │
|
|
21
|
-
│ │ Widget │ │
|
|
22
|
-
│ │ (ESM import) │ │
|
|
21
|
+
│ │ Widget │ │ (ESM import) │ │ Widget │ │
|
|
22
|
+
│ │ (ESM import) │ │ │ │ (ESM import) │ │
|
|
23
23
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
24
24
|
│ ↑ ↑ ↑ │
|
|
25
25
|
│ │ │ │ │
|
|
@@ -56,7 +56,7 @@ interface SmartLinksWidgetProps {
|
|
|
56
56
|
SL: typeof import('@proveanything/smartlinks');
|
|
57
57
|
|
|
58
58
|
// Callback to navigate within the parent application
|
|
59
|
-
onNavigate?: (
|
|
59
|
+
onNavigate?: (request: NavigationRequest | string) => void;
|
|
60
60
|
|
|
61
61
|
// Base URL to the full public portal for deep linking
|
|
62
62
|
publicPortalUrl?: string;
|
|
@@ -80,22 +80,102 @@ interface SmartLinksWidgetProps {
|
|
|
80
80
|
| `proofId` | `string?` | Optional proof context |
|
|
81
81
|
| `user` | `object?` | Current user info if authenticated |
|
|
82
82
|
| `SL` | `typeof SL` | Pre-initialized SmartLinks SDK |
|
|
83
|
-
| `onNavigate` | `function?` | Callback to navigate within parent app |
|
|
83
|
+
| `onNavigate` | `function?` | Callback to navigate within parent app (accepts `NavigationRequest` or legacy string) |
|
|
84
84
|
| `publicPortalUrl` | `string?` | Base URL to full portal for deep links |
|
|
85
85
|
| `size` | `string?` | Size hint: 'compact', 'standard', or 'large' |
|
|
86
86
|
| `lang` | `string?` | Language code (e.g., 'en', 'de', 'fr') |
|
|
87
87
|
| `translations` | `object?` | Translation overrides |
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Cross-App Navigation
|
|
92
|
+
|
|
93
|
+
Widgets can navigate to other apps within the portal using **structured navigation requests**. This allows a widget to say "open app X with these parameters" without knowing the portal's URL structure or hierarchy state. The portal orchestrator receives the request, preserves the current context (collection, product, proof, theme, auth), and performs the actual navigation.
|
|
94
|
+
|
|
95
|
+
### NavigationRequest
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
interface NavigationRequest {
|
|
99
|
+
/** Target app ID to activate */
|
|
100
|
+
appId: string;
|
|
101
|
+
/** Deep link / page within the target app (forwarded as pageId) */
|
|
102
|
+
deepLink?: string;
|
|
103
|
+
/** Extra params forwarded to the target app (e.g. { campaignId: '123' }) */
|
|
104
|
+
params?: Record<string, string>;
|
|
105
|
+
/** Optionally switch to a specific product before showing the app */
|
|
106
|
+
productId?: string;
|
|
107
|
+
/** Optionally switch to a specific proof before showing the app */
|
|
108
|
+
proofId?: string;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Usage Examples
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const MyWidget: React.FC<SmartLinksWidgetProps> = ({ onNavigate, ...props }) => {
|
|
116
|
+
|
|
117
|
+
// Navigate to another app, keeping current context
|
|
118
|
+
const handleEnterCompetition = () => {
|
|
119
|
+
onNavigate?.({
|
|
120
|
+
appId: 'competition-app',
|
|
121
|
+
deepLink: 'enter',
|
|
122
|
+
params: { campaignId: '123', ref: 'widget' },
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Navigate to a specific product's app
|
|
127
|
+
const handleViewProduct = (productId: string) => {
|
|
128
|
+
onNavigate?.({
|
|
129
|
+
appId: 'product-info',
|
|
130
|
+
productId,
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Navigate to a proof-level app
|
|
135
|
+
const handleViewWarranty = (proofId: string) => {
|
|
136
|
+
onNavigate?.({
|
|
137
|
+
appId: 'warranty-app',
|
|
138
|
+
proofId,
|
|
139
|
+
productId: 'prod-123', // required when jumping to proof level
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Card>
|
|
145
|
+
<Button onClick={handleEnterCompetition}>Enter Competition</Button>
|
|
146
|
+
<Button onClick={() => handleViewProduct('prod-456')}>View Product</Button>
|
|
147
|
+
</Card>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### How It Works End-to-End
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
Widget clicks "Enter Competition"
|
|
156
|
+
→ onNavigate({ appId: 'competition', deepLink: 'enter', params: { ref: 'widget' } })
|
|
157
|
+
→ Portal orchestrator receives NavigationRequest
|
|
158
|
+
→ Calls actions.navigateToApp('competition', 'enter')
|
|
159
|
+
→ Orchestrator renders the target app with extraParams: { pageId: 'enter', ref: 'widget' }
|
|
160
|
+
→ Current collectionId, productId, proofId, theme all preserved automatically
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Backward Compatibility
|
|
164
|
+
|
|
165
|
+
The `onNavigate` callback accepts both structured `NavigationRequest` objects and legacy strings. Existing widgets that call `onNavigate('/some-path')` continue to work — the portal treats plain strings as legacy no-ops and logs them for debugging.
|
|
166
|
+
|
|
167
|
+
**New widgets should always use the structured `NavigationRequest` format.**
|
|
168
|
+
|
|
169
|
+
### onNavigate vs publicPortalUrl
|
|
90
170
|
|
|
91
171
|
Widgets support two navigation patterns:
|
|
92
172
|
|
|
93
|
-
**`onNavigate` (parent-controlled)**
|
|
94
|
-
- Parent provides a callback
|
|
95
|
-
- Widget
|
|
96
|
-
-
|
|
173
|
+
**`onNavigate` (parent-controlled, recommended)**
|
|
174
|
+
- Parent provides a callback that the orchestrator interprets
|
|
175
|
+
- Widget emits a structured `NavigationRequest`
|
|
176
|
+
- Portal handles hierarchy transitions, context preservation, and routing
|
|
97
177
|
|
|
98
|
-
**`publicPortalUrl` (direct redirect)**
|
|
178
|
+
**`publicPortalUrl` (direct redirect, escape hatch)**
|
|
99
179
|
- Widget knows the full URL to the public portal
|
|
100
180
|
- Uses `SL.iframe.redirectParent()` for navigation
|
|
101
181
|
- Automatically handles iframe escape via postMessage
|
|
@@ -104,16 +184,17 @@ Widgets support two navigation patterns:
|
|
|
104
184
|
**Priority:** If both are provided, `onNavigate` takes precedence.
|
|
105
185
|
|
|
106
186
|
```typescript
|
|
107
|
-
//
|
|
187
|
+
// Recommended: structured navigation
|
|
108
188
|
<MyWidget
|
|
109
|
-
onNavigate={(
|
|
110
|
-
|
|
189
|
+
onNavigate={(request) => {
|
|
190
|
+
// Portal orchestrator handles this automatically
|
|
191
|
+
// when using ContentOrchestrator / OrchestratedPortal
|
|
192
|
+
}}
|
|
111
193
|
/>
|
|
112
194
|
|
|
113
|
-
//
|
|
195
|
+
// Escape hatch: direct redirect
|
|
114
196
|
<MyWidget
|
|
115
197
|
publicPortalUrl="https://my-app.smartlinks.io"
|
|
116
|
-
// ...
|
|
117
198
|
/>
|
|
118
199
|
```
|
|
119
200
|
|
|
@@ -136,7 +217,6 @@ export const MyWidget: React.FC<SmartLinksWidgetProps> = ({
|
|
|
136
217
|
user,
|
|
137
218
|
SL,
|
|
138
219
|
onNavigate,
|
|
139
|
-
publicPortalUrl,
|
|
140
220
|
size = 'standard'
|
|
141
221
|
}) => {
|
|
142
222
|
// Use the SL SDK for API calls
|
|
@@ -148,19 +228,13 @@ export const MyWidget: React.FC<SmartLinksWidgetProps> = ({
|
|
|
148
228
|
console.log('Config:', data);
|
|
149
229
|
};
|
|
150
230
|
|
|
151
|
-
// Navigate to
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// Parent-controlled navigation
|
|
159
|
-
onNavigate(relativePath);
|
|
160
|
-
} else if (publicPortalUrl) {
|
|
161
|
-
// Direct redirect (handles iframe escape automatically)
|
|
162
|
-
SL.iframe.redirectParent(`${publicPortalUrl}${relativePath}`);
|
|
163
|
-
}
|
|
231
|
+
// Navigate to another app using structured request
|
|
232
|
+
const handleOpenFullApp = () => {
|
|
233
|
+
onNavigate?.({
|
|
234
|
+
appId,
|
|
235
|
+
deepLink: 'details',
|
|
236
|
+
params: { source: 'widget' },
|
|
237
|
+
});
|
|
164
238
|
};
|
|
165
239
|
|
|
166
240
|
return (
|
|
@@ -172,7 +246,7 @@ export const MyWidget: React.FC<SmartLinksWidgetProps> = ({
|
|
|
172
246
|
<p className="text-muted-foreground mb-4">
|
|
173
247
|
Widget content goes here
|
|
174
248
|
</p>
|
|
175
|
-
<Button onClick={
|
|
249
|
+
<Button onClick={handleOpenFullApp}>Open App</Button>
|
|
176
250
|
</CardContent>
|
|
177
251
|
</Card>
|
|
178
252
|
);
|
|
@@ -316,23 +390,15 @@ const CompetitionWidget = lazy(() =>
|
|
|
316
390
|
function Portal() {
|
|
317
391
|
return (
|
|
318
392
|
<Suspense fallback={<WidgetSkeleton />}>
|
|
319
|
-
{/* Option 1: Parent controls navigation */}
|
|
320
|
-
<CompetitionWidget
|
|
321
|
-
collectionId="abc123"
|
|
322
|
-
appId="competition"
|
|
323
|
-
user={currentUser}
|
|
324
|
-
SL={SL}
|
|
325
|
-
onNavigate={(path) => window.open(`https://competition-app.example.com${path}`)}
|
|
326
|
-
size="standard"
|
|
327
|
-
/>
|
|
328
|
-
|
|
329
|
-
{/* Option 2: Widget handles its own navigation */}
|
|
330
393
|
<CompetitionWidget
|
|
331
394
|
collectionId="abc123"
|
|
332
395
|
appId="competition"
|
|
333
396
|
user={currentUser}
|
|
334
397
|
SL={SL}
|
|
335
|
-
|
|
398
|
+
onNavigate={(request) => {
|
|
399
|
+
// The portal orchestrator handles NavigationRequest objects
|
|
400
|
+
// automatically when using ContentOrchestrator / OrchestratedPortal
|
|
401
|
+
}}
|
|
336
402
|
size="standard"
|
|
337
403
|
/>
|
|
338
404
|
</Suspense>
|
|
@@ -411,7 +477,8 @@ Widgets use semantic class names that reference these variables:
|
|
|
411
477
|
- ✅ Use semantic color classes for theming
|
|
412
478
|
- ✅ Handle loading and error states gracefully
|
|
413
479
|
- ✅ Use the provided `SL` SDK for API calls
|
|
414
|
-
- ✅
|
|
480
|
+
- ✅ Use structured `NavigationRequest` for cross-app navigation
|
|
481
|
+
- ✅ Include `params` for any extra context the target app needs
|
|
415
482
|
|
|
416
483
|
### Don'ts
|
|
417
484
|
|
|
@@ -420,6 +487,7 @@ Widgets use semantic class names that reference these variables:
|
|
|
420
487
|
- ❌ Don't assume specific viewport sizes
|
|
421
488
|
- ❌ Don't make widgets too complex (use full app for that)
|
|
422
489
|
- ❌ Don't store state that should persist (use parent or SDK)
|
|
490
|
+
- ❌ Don't construct portal URLs manually — use `NavigationRequest` instead
|
|
423
491
|
|
|
424
492
|
---
|
|
425
493
|
|
|
@@ -495,7 +563,7 @@ function WidgetTestPage() {
|
|
|
495
563
|
```
|
|
496
564
|
src/widgets/
|
|
497
565
|
├── index.ts # Main exports barrel
|
|
498
|
-
├── types.ts # SmartLinksWidgetProps and related types
|
|
566
|
+
├── types.ts # SmartLinksWidgetProps, NavigationRequest, and related types
|
|
499
567
|
├── WidgetWrapper.tsx # Error boundary + Suspense wrapper
|
|
500
568
|
└── ExampleWidget/
|
|
501
569
|
├── index.tsx # Re-export
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
* for inline content first and fall back to loading the URL.
|
|
7
7
|
*
|
|
8
8
|
* if (bundle.source) {
|
|
9
|
-
* // inline JS
|
|
9
|
+
* // inline JS -- create a Blob URL or inject directly
|
|
10
10
|
* } else if (bundle.js) {
|
|
11
11
|
* // load from URL
|
|
12
12
|
* }
|
|
13
13
|
*/
|
|
14
14
|
export interface AppBundle {
|
|
15
|
-
/** URL to the JavaScript file
|
|
15
|
+
/** URL to the JavaScript file -- load if `source` is absent */
|
|
16
16
|
js: string | null;
|
|
17
|
-
/** URL to the CSS file
|
|
17
|
+
/** URL to the CSS file -- load if `styles` is absent */
|
|
18
18
|
css: string | null;
|
|
19
19
|
/** Inlined JavaScript source (present when server bundles it inline) */
|
|
20
20
|
source?: string;
|
|
@@ -26,12 +26,12 @@ export interface AppBundle {
|
|
|
26
26
|
*/
|
|
27
27
|
export interface AppManifestFiles {
|
|
28
28
|
js: {
|
|
29
|
-
/** UMD bundle
|
|
29
|
+
/** UMD bundle -- used for script-tag / dynamic loading */
|
|
30
30
|
umd: string;
|
|
31
|
-
/** ESM bundle
|
|
31
|
+
/** ESM bundle -- used for native ES module loading */
|
|
32
32
|
esm?: string;
|
|
33
33
|
};
|
|
34
|
-
/** CSS file
|
|
34
|
+
/** CSS file -- absent if the bundle ships no styles */
|
|
35
35
|
css?: string;
|
|
36
36
|
}
|
|
37
37
|
/** A single widget component defined in the manifest */
|
|
@@ -56,32 +56,22 @@ export interface AppContainerComponent {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* Shape of `app.admin.json` -- the separate admin configuration file pointed to
|
|
60
|
+
* by `AppManifest.admin`. Fetch this file yourself when you need setup / import /
|
|
61
|
+
* tunable / metrics details; it is not inlined in the manifest.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const adminUrl = new URL(manifest.admin!, appBaseUrl);
|
|
65
|
+
* const adminConfig: AppAdminConfig = await fetch(adminUrl).then(r => r.json());
|
|
60
66
|
*/
|
|
61
|
-
export interface
|
|
67
|
+
export interface AppAdminConfig {
|
|
62
68
|
$schema?: string;
|
|
63
|
-
meta?: {
|
|
64
|
-
name: string;
|
|
65
|
-
description?: string;
|
|
66
|
-
version: string;
|
|
67
|
-
platformRevision?: string;
|
|
68
|
-
appId: string;
|
|
69
|
-
};
|
|
70
69
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* Path (relative to the app's public root) to an AI guide markdown file.
|
|
71
|
+
* Provides natural-language context for AI-assisted configuration.
|
|
72
|
+
* @example "ai-guide.md"
|
|
73
73
|
*/
|
|
74
|
-
|
|
75
|
-
/** Widget bundle definition. Presence means a widget bundle exists for this app. */
|
|
76
|
-
widgets?: {
|
|
77
|
-
files: AppManifestFiles;
|
|
78
|
-
components: AppWidgetComponent[];
|
|
79
|
-
};
|
|
80
|
-
/** Container bundle definition. Presence means a container bundle exists. */
|
|
81
|
-
containers?: {
|
|
82
|
-
files: AppManifestFiles;
|
|
83
|
-
components: AppContainerComponent[];
|
|
84
|
-
};
|
|
74
|
+
aiGuide?: string;
|
|
85
75
|
setup?: {
|
|
86
76
|
description?: string;
|
|
87
77
|
questions?: Array<{
|
|
@@ -144,6 +134,39 @@ export interface AppManifest {
|
|
|
144
134
|
compute?: string;
|
|
145
135
|
}>;
|
|
146
136
|
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* SmartLinks App Manifest -- the app.manifest.json structure.
|
|
140
|
+
*
|
|
141
|
+
* Setup, import, tunable, and metrics configuration lives in a separate
|
|
142
|
+
* `app.admin.json` file. Use the `admin` field to locate and fetch it.
|
|
143
|
+
*/
|
|
144
|
+
export interface AppManifest {
|
|
145
|
+
$schema?: string;
|
|
146
|
+
meta?: {
|
|
147
|
+
name: string;
|
|
148
|
+
description?: string;
|
|
149
|
+
version: string;
|
|
150
|
+
platformRevision?: string;
|
|
151
|
+
appId: string;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Relative path to the admin configuration file (e.g. `"app.admin.json"`).
|
|
155
|
+
* When present, fetch this file to get the full {@link AppAdminConfig}
|
|
156
|
+
* (setup questions, import schema, tunable fields, metrics definitions).
|
|
157
|
+
* Absent when the app has no admin UI.
|
|
158
|
+
*/
|
|
159
|
+
admin?: string;
|
|
160
|
+
/** Widget bundle definition. Presence means a widget bundle exists for this app. */
|
|
161
|
+
widgets?: {
|
|
162
|
+
files: AppManifestFiles;
|
|
163
|
+
components: AppWidgetComponent[];
|
|
164
|
+
};
|
|
165
|
+
/** Container bundle definition. Presence means a container bundle exists. */
|
|
166
|
+
containers?: {
|
|
167
|
+
files: AppManifestFiles;
|
|
168
|
+
components: AppContainerComponent[];
|
|
169
|
+
};
|
|
147
170
|
[key: string]: any;
|
|
148
171
|
}
|
|
149
172
|
/**
|
|
@@ -152,11 +175,11 @@ export interface AppManifest {
|
|
|
152
175
|
export interface CollectionAppWidget {
|
|
153
176
|
appId: string;
|
|
154
177
|
manifest: AppManifest;
|
|
155
|
-
/** Widget bundle
|
|
178
|
+
/** Widget bundle -- always present (apps without widgets are excluded from the response) */
|
|
156
179
|
widget: AppBundle;
|
|
157
|
-
/** Container bundle
|
|
180
|
+
/** Container bundle -- null when the app has no containers */
|
|
158
181
|
container: AppBundle | null;
|
|
159
|
-
/** URL to the admin configuration JSON
|
|
182
|
+
/** URL to the admin configuration JSON -- null when the app has no admin config */
|
|
160
183
|
admin: string | null;
|
|
161
184
|
}
|
|
162
185
|
/**
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.7 | Generated: 2026-02-21T14:49:14.374Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -869,28 +869,14 @@ interface AppContainerComponent {
|
|
|
869
869
|
}
|
|
870
870
|
```
|
|
871
871
|
|
|
872
|
-
**
|
|
872
|
+
**AppAdminConfig** (interface)
|
|
873
873
|
```typescript
|
|
874
|
-
interface
|
|
874
|
+
interface AppAdminConfig {
|
|
875
875
|
$schema?: string;
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
platformRevision?: string;
|
|
881
|
-
appId: string;
|
|
882
|
-
};
|
|
883
|
-
* Filename of the admin configuration JSON (e.g. "app.admin.json").
|
|
884
|
-
* Present when the app ships an admin UI config.
|
|
885
|
-
admin?: string;
|
|
886
|
-
widgets?: {
|
|
887
|
-
files: AppManifestFiles;
|
|
888
|
-
components: AppWidgetComponent[];
|
|
889
|
-
};
|
|
890
|
-
containers?: {
|
|
891
|
-
files: AppManifestFiles;
|
|
892
|
-
components: AppContainerComponent[];
|
|
893
|
-
};
|
|
876
|
+
* Path (relative to the app's public root) to an AI guide markdown file.
|
|
877
|
+
* Provides natural-language context for AI-assisted configuration.
|
|
878
|
+
* @example "ai-guide.md"
|
|
879
|
+
aiGuide?: string;
|
|
894
880
|
setup?: {
|
|
895
881
|
description?: string;
|
|
896
882
|
questions?: Array<{
|
|
@@ -944,6 +930,33 @@ interface AppManifest {
|
|
|
944
930
|
interactions?: Array<{ id: string; description?: string }>;
|
|
945
931
|
kpis?: Array<{ name: string; compute?: string }>;
|
|
946
932
|
};
|
|
933
|
+
}
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
**AppManifest** (interface)
|
|
937
|
+
```typescript
|
|
938
|
+
interface AppManifest {
|
|
939
|
+
$schema?: string;
|
|
940
|
+
meta?: {
|
|
941
|
+
name: string;
|
|
942
|
+
description?: string;
|
|
943
|
+
version: string;
|
|
944
|
+
platformRevision?: string;
|
|
945
|
+
appId: string;
|
|
946
|
+
};
|
|
947
|
+
* Relative path to the admin configuration file (e.g. `"app.admin.json"`).
|
|
948
|
+
* When present, fetch this file to get the full {@link AppAdminConfig}
|
|
949
|
+
* (setup questions, import schema, tunable fields, metrics definitions).
|
|
950
|
+
* Absent when the app has no admin UI.
|
|
951
|
+
admin?: string;
|
|
952
|
+
widgets?: {
|
|
953
|
+
files: AppManifestFiles;
|
|
954
|
+
components: AppWidgetComponent[];
|
|
955
|
+
};
|
|
956
|
+
containers?: {
|
|
957
|
+
files: AppManifestFiles;
|
|
958
|
+
components: AppContainerComponent[];
|
|
959
|
+
};
|
|
947
960
|
[key: string]: any;
|
|
948
961
|
}
|
|
949
962
|
```
|
package/docs/containers.md
CHANGED
|
@@ -47,7 +47,7 @@ All standard widget props apply:
|
|
|
47
47
|
| `productId` | string | ❌ | Product context |
|
|
48
48
|
| `proofId` | string | ❌ | Proof context |
|
|
49
49
|
| `user` | object | ❌ | Current user info |
|
|
50
|
-
| `onNavigate` | function | ❌ | Navigation callback
|
|
50
|
+
| `onNavigate` | function | ❌ | Navigation callback (accepts `NavigationRequest` or legacy string) |
|
|
51
51
|
| `size` | string | ❌ | `"compact"`, `"standard"`, or `"large"` |
|
|
52
52
|
| `lang` | string | ❌ | Language code (e.g., `"en"`) |
|
|
53
53
|
| `translations` | object | ❌ | Translation overrides |
|
|
@@ -55,6 +55,67 @@ All standard widget props apply:
|
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
58
|
+
## Cross-App Navigation
|
|
59
|
+
|
|
60
|
+
Containers support the same **structured navigation requests** as widgets. When a container needs to navigate to another app within the portal, it emits a `NavigationRequest` via the `onNavigate` callback. The portal orchestrator interprets the request, preserves hierarchy context, and performs the navigation.
|
|
61
|
+
|
|
62
|
+
### NavigationRequest
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
interface NavigationRequest {
|
|
66
|
+
/** Target app ID to activate */
|
|
67
|
+
appId: string;
|
|
68
|
+
/** Deep link / page within the target app (forwarded as pageId) */
|
|
69
|
+
deepLink?: string;
|
|
70
|
+
/** Extra params forwarded to the target app */
|
|
71
|
+
params?: Record<string, string>;
|
|
72
|
+
/** Optionally switch to a specific product before showing the app */
|
|
73
|
+
productId?: string;
|
|
74
|
+
/** Optionally switch to a specific proof before showing the app */
|
|
75
|
+
proofId?: string;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Usage in Containers
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Inside a container component
|
|
83
|
+
const handleNavigateToWarranty = () => {
|
|
84
|
+
onNavigate?.({
|
|
85
|
+
appId: 'warranty-app',
|
|
86
|
+
deepLink: 'register',
|
|
87
|
+
params: { source: 'product-page' },
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Switch product context and open an app
|
|
92
|
+
const handleViewRelatedProduct = (productId: string) => {
|
|
93
|
+
onNavigate?.({
|
|
94
|
+
appId: 'product-info',
|
|
95
|
+
productId,
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### How It Works
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
Container button "Register Warranty"
|
|
104
|
+
→ onNavigate({ appId: 'warranty', deepLink: 'register', params: { ref: 'container' } })
|
|
105
|
+
→ Portal orchestrator receives NavigationRequest
|
|
106
|
+
→ Calls actions.navigateToApp('warranty', 'register')
|
|
107
|
+
→ Target app loads with extraParams: { pageId: 'register', ref: 'container' }
|
|
108
|
+
→ collectionId, productId, proofId, theme all preserved automatically
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Backward Compatibility
|
|
112
|
+
|
|
113
|
+
The `onNavigate` callback accepts both structured `NavigationRequest` objects and legacy strings. Existing containers that call `onNavigate('/some-path')` continue to work. **New containers should always use the structured `NavigationRequest` format.**
|
|
114
|
+
|
|
115
|
+
See `widgets.md` for the full `NavigationRequest` documentation and additional examples.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
58
119
|
## Architecture
|
|
59
120
|
|
|
60
121
|
Containers use **MemoryRouter** (not HashRouter) because the parent app owns the browser's URL bar. Context is passed via props rather than URL parameters. Each container gets its own `QueryClient` to avoid cache collisions with the parent app.
|
|
@@ -83,7 +144,10 @@ const { PublicContainer } = await import('https://my-app.com/containers.es.js');
|
|
|
83
144
|
appId="my-app"
|
|
84
145
|
productId="prod-123"
|
|
85
146
|
SL={SL}
|
|
86
|
-
onNavigate={
|
|
147
|
+
onNavigate={(request) => {
|
|
148
|
+
// The portal orchestrator handles NavigationRequest objects
|
|
149
|
+
// automatically when using ContentOrchestrator / OrchestratedPortal
|
|
150
|
+
}}
|
|
87
151
|
lang="en"
|
|
88
152
|
className="max-w-4xl mx-auto"
|
|
89
153
|
/>
|
|
@@ -184,6 +248,7 @@ src/containers/
|
|
|
184
248
|
| **Styling** | Fully isolated CSS | Inherits parent CSS variables |
|
|
185
249
|
| **Communication** | postMessage | Direct props / callbacks |
|
|
186
250
|
| **Auth** | Via `SL.auth.getAccount()` | Via `user` prop from parent |
|
|
251
|
+
| **Navigation** | `window.parent.postMessage` | `onNavigate` with `NavigationRequest` |
|
|
187
252
|
|
|
188
253
|
---
|
|
189
254
|
|
|
@@ -196,3 +261,4 @@ src/containers/
|
|
|
196
261
|
| Routing doesn't work | Using HashRouter instead of MemoryRouter | Containers must use MemoryRouter |
|
|
197
262
|
| Query cache conflicts | Sharing parent's QueryClient | Each container needs its own `QueryClient` instance |
|
|
198
263
|
| `cva.cva` runtime error | Global set to lowercase `cva` | Use uppercase `CVA` for the global name |
|
|
264
|
+
| Navigation does nothing | Using legacy string with `onNavigate` | Use structured `NavigationRequest` object instead |
|
package/docs/widgets.md
CHANGED
|
@@ -10,7 +10,7 @@ Widgets are self-contained React components that:
|
|
|
10
10
|
- Run inside the parent React application (not iframes)
|
|
11
11
|
- Receive standardized context props
|
|
12
12
|
- Inherit styling from the parent via CSS variables
|
|
13
|
-
- Can trigger navigation
|
|
13
|
+
- Can trigger structured cross-app navigation within the parent portal
|
|
14
14
|
|
|
15
15
|
```text
|
|
16
16
|
┌─────────────────────────────────────────────────────────────────┐
|
|
@@ -18,8 +18,8 @@ Widgets are self-contained React components that:
|
|
|
18
18
|
│ │
|
|
19
19
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
20
20
|
│ │ Competition │ │ Music App │ │ Warranty │ │
|
|
21
|
-
│ │ Widget │ │
|
|
22
|
-
│ │ (ESM import) │ │
|
|
21
|
+
│ │ Widget │ │ (ESM import) │ │ Widget │ │
|
|
22
|
+
│ │ (ESM import) │ │ │ │ (ESM import) │ │
|
|
23
23
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
24
24
|
│ ↑ ↑ ↑ │
|
|
25
25
|
│ │ │ │ │
|
|
@@ -56,7 +56,7 @@ interface SmartLinksWidgetProps {
|
|
|
56
56
|
SL: typeof import('@proveanything/smartlinks');
|
|
57
57
|
|
|
58
58
|
// Callback to navigate within the parent application
|
|
59
|
-
onNavigate?: (
|
|
59
|
+
onNavigate?: (request: NavigationRequest | string) => void;
|
|
60
60
|
|
|
61
61
|
// Base URL to the full public portal for deep linking
|
|
62
62
|
publicPortalUrl?: string;
|
|
@@ -80,22 +80,102 @@ interface SmartLinksWidgetProps {
|
|
|
80
80
|
| `proofId` | `string?` | Optional proof context |
|
|
81
81
|
| `user` | `object?` | Current user info if authenticated |
|
|
82
82
|
| `SL` | `typeof SL` | Pre-initialized SmartLinks SDK |
|
|
83
|
-
| `onNavigate` | `function?` | Callback to navigate within parent app |
|
|
83
|
+
| `onNavigate` | `function?` | Callback to navigate within parent app (accepts `NavigationRequest` or legacy string) |
|
|
84
84
|
| `publicPortalUrl` | `string?` | Base URL to full portal for deep links |
|
|
85
85
|
| `size` | `string?` | Size hint: 'compact', 'standard', or 'large' |
|
|
86
86
|
| `lang` | `string?` | Language code (e.g., 'en', 'de', 'fr') |
|
|
87
87
|
| `translations` | `object?` | Translation overrides |
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Cross-App Navigation
|
|
92
|
+
|
|
93
|
+
Widgets can navigate to other apps within the portal using **structured navigation requests**. This allows a widget to say "open app X with these parameters" without knowing the portal's URL structure or hierarchy state. The portal orchestrator receives the request, preserves the current context (collection, product, proof, theme, auth), and performs the actual navigation.
|
|
94
|
+
|
|
95
|
+
### NavigationRequest
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
interface NavigationRequest {
|
|
99
|
+
/** Target app ID to activate */
|
|
100
|
+
appId: string;
|
|
101
|
+
/** Deep link / page within the target app (forwarded as pageId) */
|
|
102
|
+
deepLink?: string;
|
|
103
|
+
/** Extra params forwarded to the target app (e.g. { campaignId: '123' }) */
|
|
104
|
+
params?: Record<string, string>;
|
|
105
|
+
/** Optionally switch to a specific product before showing the app */
|
|
106
|
+
productId?: string;
|
|
107
|
+
/** Optionally switch to a specific proof before showing the app */
|
|
108
|
+
proofId?: string;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Usage Examples
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const MyWidget: React.FC<SmartLinksWidgetProps> = ({ onNavigate, ...props }) => {
|
|
116
|
+
|
|
117
|
+
// Navigate to another app, keeping current context
|
|
118
|
+
const handleEnterCompetition = () => {
|
|
119
|
+
onNavigate?.({
|
|
120
|
+
appId: 'competition-app',
|
|
121
|
+
deepLink: 'enter',
|
|
122
|
+
params: { campaignId: '123', ref: 'widget' },
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Navigate to a specific product's app
|
|
127
|
+
const handleViewProduct = (productId: string) => {
|
|
128
|
+
onNavigate?.({
|
|
129
|
+
appId: 'product-info',
|
|
130
|
+
productId,
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Navigate to a proof-level app
|
|
135
|
+
const handleViewWarranty = (proofId: string) => {
|
|
136
|
+
onNavigate?.({
|
|
137
|
+
appId: 'warranty-app',
|
|
138
|
+
proofId,
|
|
139
|
+
productId: 'prod-123', // required when jumping to proof level
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Card>
|
|
145
|
+
<Button onClick={handleEnterCompetition}>Enter Competition</Button>
|
|
146
|
+
<Button onClick={() => handleViewProduct('prod-456')}>View Product</Button>
|
|
147
|
+
</Card>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### How It Works End-to-End
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
Widget clicks "Enter Competition"
|
|
156
|
+
→ onNavigate({ appId: 'competition', deepLink: 'enter', params: { ref: 'widget' } })
|
|
157
|
+
→ Portal orchestrator receives NavigationRequest
|
|
158
|
+
→ Calls actions.navigateToApp('competition', 'enter')
|
|
159
|
+
→ Orchestrator renders the target app with extraParams: { pageId: 'enter', ref: 'widget' }
|
|
160
|
+
→ Current collectionId, productId, proofId, theme all preserved automatically
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Backward Compatibility
|
|
164
|
+
|
|
165
|
+
The `onNavigate` callback accepts both structured `NavigationRequest` objects and legacy strings. Existing widgets that call `onNavigate('/some-path')` continue to work — the portal treats plain strings as legacy no-ops and logs them for debugging.
|
|
166
|
+
|
|
167
|
+
**New widgets should always use the structured `NavigationRequest` format.**
|
|
168
|
+
|
|
169
|
+
### onNavigate vs publicPortalUrl
|
|
90
170
|
|
|
91
171
|
Widgets support two navigation patterns:
|
|
92
172
|
|
|
93
|
-
**`onNavigate` (parent-controlled)**
|
|
94
|
-
- Parent provides a callback
|
|
95
|
-
- Widget
|
|
96
|
-
-
|
|
173
|
+
**`onNavigate` (parent-controlled, recommended)**
|
|
174
|
+
- Parent provides a callback that the orchestrator interprets
|
|
175
|
+
- Widget emits a structured `NavigationRequest`
|
|
176
|
+
- Portal handles hierarchy transitions, context preservation, and routing
|
|
97
177
|
|
|
98
|
-
**`publicPortalUrl` (direct redirect)**
|
|
178
|
+
**`publicPortalUrl` (direct redirect, escape hatch)**
|
|
99
179
|
- Widget knows the full URL to the public portal
|
|
100
180
|
- Uses `SL.iframe.redirectParent()` for navigation
|
|
101
181
|
- Automatically handles iframe escape via postMessage
|
|
@@ -104,16 +184,17 @@ Widgets support two navigation patterns:
|
|
|
104
184
|
**Priority:** If both are provided, `onNavigate` takes precedence.
|
|
105
185
|
|
|
106
186
|
```typescript
|
|
107
|
-
//
|
|
187
|
+
// Recommended: structured navigation
|
|
108
188
|
<MyWidget
|
|
109
|
-
onNavigate={(
|
|
110
|
-
|
|
189
|
+
onNavigate={(request) => {
|
|
190
|
+
// Portal orchestrator handles this automatically
|
|
191
|
+
// when using ContentOrchestrator / OrchestratedPortal
|
|
192
|
+
}}
|
|
111
193
|
/>
|
|
112
194
|
|
|
113
|
-
//
|
|
195
|
+
// Escape hatch: direct redirect
|
|
114
196
|
<MyWidget
|
|
115
197
|
publicPortalUrl="https://my-app.smartlinks.io"
|
|
116
|
-
// ...
|
|
117
198
|
/>
|
|
118
199
|
```
|
|
119
200
|
|
|
@@ -136,7 +217,6 @@ export const MyWidget: React.FC<SmartLinksWidgetProps> = ({
|
|
|
136
217
|
user,
|
|
137
218
|
SL,
|
|
138
219
|
onNavigate,
|
|
139
|
-
publicPortalUrl,
|
|
140
220
|
size = 'standard'
|
|
141
221
|
}) => {
|
|
142
222
|
// Use the SL SDK for API calls
|
|
@@ -148,19 +228,13 @@ export const MyWidget: React.FC<SmartLinksWidgetProps> = ({
|
|
|
148
228
|
console.log('Config:', data);
|
|
149
229
|
};
|
|
150
230
|
|
|
151
|
-
// Navigate to
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// Parent-controlled navigation
|
|
159
|
-
onNavigate(relativePath);
|
|
160
|
-
} else if (publicPortalUrl) {
|
|
161
|
-
// Direct redirect (handles iframe escape automatically)
|
|
162
|
-
SL.iframe.redirectParent(`${publicPortalUrl}${relativePath}`);
|
|
163
|
-
}
|
|
231
|
+
// Navigate to another app using structured request
|
|
232
|
+
const handleOpenFullApp = () => {
|
|
233
|
+
onNavigate?.({
|
|
234
|
+
appId,
|
|
235
|
+
deepLink: 'details',
|
|
236
|
+
params: { source: 'widget' },
|
|
237
|
+
});
|
|
164
238
|
};
|
|
165
239
|
|
|
166
240
|
return (
|
|
@@ -172,7 +246,7 @@ export const MyWidget: React.FC<SmartLinksWidgetProps> = ({
|
|
|
172
246
|
<p className="text-muted-foreground mb-4">
|
|
173
247
|
Widget content goes here
|
|
174
248
|
</p>
|
|
175
|
-
<Button onClick={
|
|
249
|
+
<Button onClick={handleOpenFullApp}>Open App</Button>
|
|
176
250
|
</CardContent>
|
|
177
251
|
</Card>
|
|
178
252
|
);
|
|
@@ -316,23 +390,15 @@ const CompetitionWidget = lazy(() =>
|
|
|
316
390
|
function Portal() {
|
|
317
391
|
return (
|
|
318
392
|
<Suspense fallback={<WidgetSkeleton />}>
|
|
319
|
-
{/* Option 1: Parent controls navigation */}
|
|
320
|
-
<CompetitionWidget
|
|
321
|
-
collectionId="abc123"
|
|
322
|
-
appId="competition"
|
|
323
|
-
user={currentUser}
|
|
324
|
-
SL={SL}
|
|
325
|
-
onNavigate={(path) => window.open(`https://competition-app.example.com${path}`)}
|
|
326
|
-
size="standard"
|
|
327
|
-
/>
|
|
328
|
-
|
|
329
|
-
{/* Option 2: Widget handles its own navigation */}
|
|
330
393
|
<CompetitionWidget
|
|
331
394
|
collectionId="abc123"
|
|
332
395
|
appId="competition"
|
|
333
396
|
user={currentUser}
|
|
334
397
|
SL={SL}
|
|
335
|
-
|
|
398
|
+
onNavigate={(request) => {
|
|
399
|
+
// The portal orchestrator handles NavigationRequest objects
|
|
400
|
+
// automatically when using ContentOrchestrator / OrchestratedPortal
|
|
401
|
+
}}
|
|
336
402
|
size="standard"
|
|
337
403
|
/>
|
|
338
404
|
</Suspense>
|
|
@@ -411,7 +477,8 @@ Widgets use semantic class names that reference these variables:
|
|
|
411
477
|
- ✅ Use semantic color classes for theming
|
|
412
478
|
- ✅ Handle loading and error states gracefully
|
|
413
479
|
- ✅ Use the provided `SL` SDK for API calls
|
|
414
|
-
- ✅
|
|
480
|
+
- ✅ Use structured `NavigationRequest` for cross-app navigation
|
|
481
|
+
- ✅ Include `params` for any extra context the target app needs
|
|
415
482
|
|
|
416
483
|
### Don'ts
|
|
417
484
|
|
|
@@ -420,6 +487,7 @@ Widgets use semantic class names that reference these variables:
|
|
|
420
487
|
- ❌ Don't assume specific viewport sizes
|
|
421
488
|
- ❌ Don't make widgets too complex (use full app for that)
|
|
422
489
|
- ❌ Don't store state that should persist (use parent or SDK)
|
|
490
|
+
- ❌ Don't construct portal URLs manually — use `NavigationRequest` instead
|
|
423
491
|
|
|
424
492
|
---
|
|
425
493
|
|
|
@@ -495,7 +563,7 @@ function WidgetTestPage() {
|
|
|
495
563
|
```
|
|
496
564
|
src/widgets/
|
|
497
565
|
├── index.ts # Main exports barrel
|
|
498
|
-
├── types.ts # SmartLinksWidgetProps and related types
|
|
566
|
+
├── types.ts # SmartLinksWidgetProps, NavigationRequest, and related types
|
|
499
567
|
├── WidgetWrapper.tsx # Error boundary + Suspense wrapper
|
|
500
568
|
└── ExampleWidget/
|
|
501
569
|
├── index.tsx # Re-export
|