@proveanything/smartlinks 1.4.6 → 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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.4.6 | Generated: 2026-02-21T09:47:56.138Z
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
 
@@ -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={handleNavigate}
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 |
@@ -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 to the full app
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 │ │ Widget │ │ Widget │ │
22
- │ │ (ESM import) │ │ (ESM import) │ │ (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?: (path: string) => void;
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
- ### Navigation: onNavigate vs publicPortalUrl
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 to handle navigation
95
- - Widget passes a relative path (e.g., `/#/?collectionId=x&tab=details`)
96
- - Parent decides what to do (router push, open modal, etc.)
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
- // Parent provides callback
187
+ // Recommended: structured navigation
108
188
  <MyWidget
109
- onNavigate={(path) => router.push(path)}
110
- // ...
189
+ onNavigate={(request) => {
190
+ // Portal orchestrator handles this automatically
191
+ // when using ContentOrchestrator / OrchestratedPortal
192
+ }}
111
193
  />
112
194
 
113
- // Widget uses direct redirect
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 full app (supports both patterns)
152
- const handleOpenApp = () => {
153
- const params = new URLSearchParams({ collectionId, appId });
154
- if (productId) params.set('productId', productId);
155
- const relativePath = `/#/?${params.toString()}`;
156
-
157
- if (onNavigate) {
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={handleOpenApp}>Open App</Button>
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
- publicPortalUrl="https://competition-app.example.com"
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
- - ✅ Provide meaningful navigation via `onNavigate`
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
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.4.6 | Generated: 2026-02-21T09:47:56.138Z
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
 
@@ -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={handleNavigate}
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 to the full app
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 │ │ Widget │ │ Widget │ │
22
- │ │ (ESM import) │ │ (ESM import) │ │ (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?: (path: string) => void;
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
- ### Navigation: onNavigate vs publicPortalUrl
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 to handle navigation
95
- - Widget passes a relative path (e.g., `/#/?collectionId=x&tab=details`)
96
- - Parent decides what to do (router push, open modal, etc.)
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
- // Parent provides callback
187
+ // Recommended: structured navigation
108
188
  <MyWidget
109
- onNavigate={(path) => router.push(path)}
110
- // ...
189
+ onNavigate={(request) => {
190
+ // Portal orchestrator handles this automatically
191
+ // when using ContentOrchestrator / OrchestratedPortal
192
+ }}
111
193
  />
112
194
 
113
- // Widget uses direct redirect
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 full app (supports both patterns)
152
- const handleOpenApp = () => {
153
- const params = new URLSearchParams({ collectionId, appId });
154
- if (productId) params.set('productId', productId);
155
- const relativePath = `/#/?${params.toString()}`;
156
-
157
- if (onNavigate) {
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={handleOpenApp}>Open App</Button>
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
- publicPortalUrl="https://competition-app.example.com"
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
- - ✅ Provide meaningful navigation via `onNavigate`
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",