@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.
- package/dist/docs/API_SUMMARY.md +1 -1
- package/dist/docs/containers.md +68 -2
- package/dist/docs/widgets.md +113 -45
- package/docs/API_SUMMARY.md +1 -1
- package/docs/containers.md +68 -2
- package/docs/widgets.md +113 -45
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
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
|
package/docs/API_SUMMARY.md
CHANGED
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
|