@proveanything/smartlinks 1.4.6 → 1.4.8
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/cache.js +21 -0
- package/dist/docs/API_SUMMARY.md +3 -2
- package/dist/docs/caching.md +186 -0
- package/dist/docs/containers.md +68 -2
- package/dist/docs/widgets.md +113 -45
- package/dist/http.d.ts +6 -0
- package/dist/http.js +34 -0
- package/docs/API_SUMMARY.md +3 -2
- package/docs/caching.md +186 -0
- package/docs/containers.md +68 -2
- package/docs/widgets.md +113 -45
- package/package.json +1 -1
package/dist/cache.js
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
// =============================================================================
|
|
4
4
|
// In-memory cache store
|
|
5
5
|
const memoryCache = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Clear session-scoped caches on page load.
|
|
8
|
+
* SessionStorage persists across normal refreshes, so we explicitly clear it
|
|
9
|
+
* on load to ensure fresh data after F5/Ctrl+F5. LocalStorage is preserved
|
|
10
|
+
* for true offline/persistent caching scenarios.
|
|
11
|
+
*/
|
|
12
|
+
function clearSessionCacheOnLoad() {
|
|
13
|
+
if (typeof sessionStorage === 'undefined')
|
|
14
|
+
return;
|
|
15
|
+
try {
|
|
16
|
+
const sessionKeys = Object.keys(sessionStorage).filter(k => k.startsWith('smartlinks:cache:'));
|
|
17
|
+
sessionKeys.forEach(k => sessionStorage.removeItem(k));
|
|
18
|
+
}
|
|
19
|
+
catch (_a) {
|
|
20
|
+
// Storage may not be available
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Auto-clear session caches on page load (browser only)
|
|
24
|
+
if (typeof window !== 'undefined') {
|
|
25
|
+
clearSessionCacheOnLoad();
|
|
26
|
+
}
|
|
6
27
|
/**
|
|
7
28
|
* Get cached value or fetch fresh.
|
|
8
29
|
*
|
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.8 | Generated: 2026-02-22T11:38:27.387Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -120,8 +120,9 @@ Returns true if the SDK currently has any auth credential set (bearer token or A
|
|
|
120
120
|
persistence?: 'none' | 'indexeddb'
|
|
121
121
|
persistenceTtlMs?: number
|
|
122
122
|
serveStaleOnOffline?: boolean
|
|
123
|
+
clearOnPageLoad?: boolean
|
|
123
124
|
}) → `void`
|
|
124
|
-
Configure the SDK's built-in in-memory GET cache. The cache is transparent — it sits inside the HTTP layer and requires no changes to your existing API calls. All GET requests benefit automatically. Per-resource rules (collections/products → 1 h, proofs → 30 s, etc.) override this value. in-memory only (`'none'`, default). Ignored in Node.js. fallback, from the original fetch time (default: 7 days). `SmartlinksOfflineError` with stale data instead of propagating the network error. ```ts // Enable IndexedDB persistence for offline support configureSdkCache({ persistence: 'indexeddb' }) // Disable cache entirely in test environments configureSdkCache({ enabled: false }) ```
|
|
125
|
+
Configure the SDK's built-in in-memory GET cache. The cache is transparent — it sits inside the HTTP layer and requires no changes to your existing API calls. All GET requests benefit automatically. Per-resource rules (collections/products → 1 h, proofs → 30 s, etc.) override this value. in-memory only (`'none'`, default). Ignored in Node.js. fallback, from the original fetch time (default: 7 days). `SmartlinksOfflineError` with stale data instead of propagating the network error. caches on page load/refresh. IndexedDB persists for offline. ```ts // Enable IndexedDB persistence for offline support configureSdkCache({ persistence: 'indexeddb' }) // Disable cache entirely in test environments configureSdkCache({ enabled: false }) // Keep caches across page refreshes (not recommended for production) configureSdkCache({ clearOnPageLoad: false }) ```
|
|
125
126
|
|
|
126
127
|
**invalidateCache**(urlPattern?: string) → `void`
|
|
127
128
|
Manually invalidate entries in the SDK's GET cache. *contains* this string is removed. Omit (or pass `undefined`) to wipe the entire cache. ```ts invalidateCache() // clear everything invalidateCache('/collection/abc123') // one specific collection invalidateCache('/product/') // all product responses ```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Caching Strategy
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Smartlinks SDK implements a multi-tier caching system designed to balance performance with data freshness:
|
|
6
|
+
|
|
7
|
+
1. **In-Memory Cache (L1)** - Fastest, cleared on page refresh
|
|
8
|
+
2. **SessionStorage** - Cleared on page refresh (not tab close)
|
|
9
|
+
3. **IndexedDB (L2)** - Persists across refreshes for offline support
|
|
10
|
+
|
|
11
|
+
## Cache Behavior on Page Refresh
|
|
12
|
+
|
|
13
|
+
### Default Behavior (Recommended)
|
|
14
|
+
|
|
15
|
+
When you refresh the page (F5, Ctrl+F5, or window reload):
|
|
16
|
+
|
|
17
|
+
- ✅ **In-memory cache** - Automatically cleared (module re-initialization)
|
|
18
|
+
- ✅ **SessionStorage** - Explicitly cleared on page load
|
|
19
|
+
- ✅ **IndexedDB** - Preserved for offline fallback only
|
|
20
|
+
|
|
21
|
+
This ensures that **page refreshes always fetch fresh data from the server**, while maintaining offline capabilities.
|
|
22
|
+
|
|
23
|
+
### How It Works
|
|
24
|
+
|
|
25
|
+
1. **During browsing (same page load)**:
|
|
26
|
+
- API requests are cached in memory and sessionStorage
|
|
27
|
+
- Subsequent identical requests serve from cache (fast!)
|
|
28
|
+
- TTL rules determine cache freshness (30s - 1h depending on resource)
|
|
29
|
+
|
|
30
|
+
2. **On page refresh**:
|
|
31
|
+
- Module re-initialization clears in-memory cache
|
|
32
|
+
- `clearSessionCacheOnLoad()` runs automatically
|
|
33
|
+
- SessionStorage caches are removed
|
|
34
|
+
- Fresh network requests are made
|
|
35
|
+
|
|
36
|
+
3. **When offline** (with `persistence: 'indexeddb'`):
|
|
37
|
+
- Network request fails
|
|
38
|
+
- SDK checks IndexedDB for stale data
|
|
39
|
+
- If found and within 7-day TTL, throws `SmartlinksOfflineError` with data
|
|
40
|
+
- App can catch this and use stale data gracefully
|
|
41
|
+
|
|
42
|
+
## Cache Configuration
|
|
43
|
+
|
|
44
|
+
### Enable IndexedDB Persistence
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { configureSdkCache } from '@smartlinks/sdk';
|
|
48
|
+
|
|
49
|
+
// Enable offline support via IndexedDB
|
|
50
|
+
configureSdkCache({
|
|
51
|
+
persistence: 'indexeddb',
|
|
52
|
+
persistenceTtlMs: 7 * 24 * 60 * 60_000, // 7 days
|
|
53
|
+
serveStaleOnOffline: true,
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Disable Clear-on-Refresh (Not Recommended)
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Keep caches across page refreshes
|
|
61
|
+
// ⚠️ Not recommended for production - you'll serve stale data after refresh
|
|
62
|
+
configureSdkCache({
|
|
63
|
+
clearOnPageLoad: false,
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Disable Caching Entirely
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Useful for testing or debugging
|
|
71
|
+
configureSdkCache({
|
|
72
|
+
enabled: false,
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Cache Storage Types
|
|
77
|
+
|
|
78
|
+
### `storage: 'memory'` (Default)
|
|
79
|
+
- Lives in JavaScript memory (Map)
|
|
80
|
+
- Cleared on page refresh
|
|
81
|
+
- Fastest access
|
|
82
|
+
- No quota limits
|
|
83
|
+
|
|
84
|
+
### `storage: 'session'`
|
|
85
|
+
- Lives in `sessionStorage`
|
|
86
|
+
- **Cleared on page load** (new behavior)
|
|
87
|
+
- Survives navigation within same session
|
|
88
|
+
- ~5-10MB quota
|
|
89
|
+
|
|
90
|
+
### `storage: 'local'`
|
|
91
|
+
- Lives in `localStorage`
|
|
92
|
+
- Persists across browser restarts
|
|
93
|
+
- Use sparingly for truly persistent data
|
|
94
|
+
- ~5-10MB quota
|
|
95
|
+
|
|
96
|
+
### `persistence: 'indexeddb'`
|
|
97
|
+
- Lives in IndexedDB (L2 cache)
|
|
98
|
+
- Persists across refreshes and restarts
|
|
99
|
+
- **Only used as offline fallback**, not for normal requests
|
|
100
|
+
- ~50MB+ quota
|
|
101
|
+
|
|
102
|
+
## TTL Rules
|
|
103
|
+
|
|
104
|
+
Different API resources have different cache lifetimes:
|
|
105
|
+
|
|
106
|
+
| Resource | TTL | Reason |
|
|
107
|
+
|----------|-----|--------|
|
|
108
|
+
| `/proof/*` | 30 seconds | Proofs change frequently |
|
|
109
|
+
| `/attestation/*` | 2 minutes | Attestations update regularly |
|
|
110
|
+
| `/product/*` | 1 hour | Products are relatively stable |
|
|
111
|
+
| `/variant/*` | 1 hour | Variants rarely change |
|
|
112
|
+
| `/collection/*` | 1 hour | Collections are stable |
|
|
113
|
+
| Everything else | 1 minute | Default safety |
|
|
114
|
+
|
|
115
|
+
## Manual Cache Control
|
|
116
|
+
|
|
117
|
+
### Invalidate Specific Resource
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { invalidateCache } from '@smartlinks/sdk';
|
|
121
|
+
|
|
122
|
+
// Clear cache for a specific collection
|
|
123
|
+
invalidateCache('/collection/abc123');
|
|
124
|
+
|
|
125
|
+
// Clear all product caches
|
|
126
|
+
invalidateCache('/product/');
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Clear All Caches
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { invalidateCache } from '@smartlinks/sdk';
|
|
133
|
+
|
|
134
|
+
// Nuclear option - clear everything
|
|
135
|
+
invalidateCache();
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Best Practices
|
|
139
|
+
|
|
140
|
+
1. **Default is best** - The SDK defaults are designed for optimal behavior
|
|
141
|
+
2. **Enable IndexedDB for PWAs** - If building offline-first apps
|
|
142
|
+
3. **Don't disable clearOnPageLoad** - Users expect fresh data after refresh
|
|
143
|
+
4. **Use invalidateCache() after mutations** - SDK does this automatically for you
|
|
144
|
+
5. **Trust the TTL rules** - They're tuned for smartlinks.app API behavior
|
|
145
|
+
|
|
146
|
+
## Offline Mode Example
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { collection, SmartlinksOfflineError } from '@smartlinks/sdk';
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const data = await collection.get('abc123');
|
|
153
|
+
// Fresh data from server
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof SmartlinksOfflineError) {
|
|
156
|
+
// Network failed, but we have stale data
|
|
157
|
+
console.warn('Offline - using stale data from', error.cachedAt);
|
|
158
|
+
const staleData = error.data;
|
|
159
|
+
// Display stale data with a banner
|
|
160
|
+
} else {
|
|
161
|
+
// Real error - no offline data available
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Migration from Old Behavior
|
|
168
|
+
|
|
169
|
+
If you previously relied on caches persisting across refreshes:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// Old (implicit) behavior:
|
|
173
|
+
// - SessionStorage survived refreshes
|
|
174
|
+
// - Apps might see stale data after F5
|
|
175
|
+
|
|
176
|
+
// New (explicit) behavior:
|
|
177
|
+
configureSdkCache({
|
|
178
|
+
clearOnPageLoad: true, // default - fresh data on refresh
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// If you REALLY need old behavior (not recommended):
|
|
182
|
+
configureSdkCache({
|
|
183
|
+
clearOnPageLoad: false,
|
|
184
|
+
persistence: 'indexeddb', // move to IndexedDB instead
|
|
185
|
+
});
|
|
186
|
+
```
|
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/dist/http.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export declare function hasAuthCredentials(): boolean;
|
|
|
82
82
|
* @param options.serveStaleOnOffline - When `true` (default) and persistence is on, throw
|
|
83
83
|
* `SmartlinksOfflineError` with stale data instead of
|
|
84
84
|
* propagating the network error.
|
|
85
|
+
* @param options.clearOnPageLoad - When `true` (default), clear in-memory and sessionStorage
|
|
86
|
+
* caches on page load/refresh. IndexedDB persists for offline.
|
|
85
87
|
*
|
|
86
88
|
* @example
|
|
87
89
|
* ```ts
|
|
@@ -90,6 +92,9 @@ export declare function hasAuthCredentials(): boolean;
|
|
|
90
92
|
*
|
|
91
93
|
* // Disable cache entirely in test environments
|
|
92
94
|
* configureSdkCache({ enabled: false })
|
|
95
|
+
*
|
|
96
|
+
* // Keep caches across page refreshes (not recommended for production)
|
|
97
|
+
* configureSdkCache({ clearOnPageLoad: false })
|
|
93
98
|
* ```
|
|
94
99
|
*/
|
|
95
100
|
export declare function configureSdkCache(options: {
|
|
@@ -99,6 +104,7 @@ export declare function configureSdkCache(options: {
|
|
|
99
104
|
persistence?: 'none' | 'indexeddb';
|
|
100
105
|
persistenceTtlMs?: number;
|
|
101
106
|
serveStaleOnOffline?: boolean;
|
|
107
|
+
clearOnPageLoad?: boolean;
|
|
102
108
|
}): void;
|
|
103
109
|
/**
|
|
104
110
|
* Manually invalidate entries in the SDK's GET cache.
|
package/dist/http.js
CHANGED
|
@@ -31,6 +31,8 @@ let cacheDefaultTtlMs = 60000; // 60 seconds
|
|
|
31
31
|
let cacheMaxEntries = 200;
|
|
32
32
|
/** Persistence backend for the L2 cache. 'none' (default) disables IndexedDB persistence. */
|
|
33
33
|
let cachePersistence = 'none';
|
|
34
|
+
/** When true (default), clear in-memory and sessionStorage caches on page load/refresh. */
|
|
35
|
+
let cacheClearOnPageLoad = true;
|
|
34
36
|
/**
|
|
35
37
|
* How long L2 (IndexedDB) entries are considered valid as an offline stale fallback,
|
|
36
38
|
* measured from the original network fetch time (default: 7 days).
|
|
@@ -89,6 +91,31 @@ function evictLruIfNeeded() {
|
|
|
89
91
|
break;
|
|
90
92
|
}
|
|
91
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Clear session-scoped caches (in-memory + sessionStorage) on page load.
|
|
96
|
+
* This ensures that a page refresh always fetches fresh data, while
|
|
97
|
+
* IndexedDB persists for true offline support.
|
|
98
|
+
*
|
|
99
|
+
* Automatically called once when this module loads in a browser environment.
|
|
100
|
+
*/
|
|
101
|
+
function clearSessionCachesOnPageLoad() {
|
|
102
|
+
if (typeof window === 'undefined')
|
|
103
|
+
return; // Node.js environment
|
|
104
|
+
httpCache.clear();
|
|
105
|
+
try {
|
|
106
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
107
|
+
const sessionKeys = Object.keys(sessionStorage).filter(k => k.startsWith('smartlinks:cache:'));
|
|
108
|
+
sessionKeys.forEach(k => sessionStorage.removeItem(k));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (_a) {
|
|
112
|
+
// Storage may not be available (private browsing, etc.)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Auto-clear session caches on page load (browser only)
|
|
116
|
+
if (typeof window !== 'undefined' && cacheClearOnPageLoad) {
|
|
117
|
+
clearSessionCachesOnPageLoad();
|
|
118
|
+
}
|
|
92
119
|
/**
|
|
93
120
|
* Return cached data for a key if it exists and is within TTL.
|
|
94
121
|
* Promotes the hit to MRU position. Returns null when missing, expired, or in-flight.
|
|
@@ -416,6 +443,8 @@ export function hasAuthCredentials() {
|
|
|
416
443
|
* @param options.serveStaleOnOffline - When `true` (default) and persistence is on, throw
|
|
417
444
|
* `SmartlinksOfflineError` with stale data instead of
|
|
418
445
|
* propagating the network error.
|
|
446
|
+
* @param options.clearOnPageLoad - When `true` (default), clear in-memory and sessionStorage
|
|
447
|
+
* caches on page load/refresh. IndexedDB persists for offline.
|
|
419
448
|
*
|
|
420
449
|
* @example
|
|
421
450
|
* ```ts
|
|
@@ -424,6 +453,9 @@ export function hasAuthCredentials() {
|
|
|
424
453
|
*
|
|
425
454
|
* // Disable cache entirely in test environments
|
|
426
455
|
* configureSdkCache({ enabled: false })
|
|
456
|
+
*
|
|
457
|
+
* // Keep caches across page refreshes (not recommended for production)
|
|
458
|
+
* configureSdkCache({ clearOnPageLoad: false })
|
|
427
459
|
* ```
|
|
428
460
|
*/
|
|
429
461
|
export function configureSdkCache(options) {
|
|
@@ -439,6 +471,8 @@ export function configureSdkCache(options) {
|
|
|
439
471
|
cachePersistenceTtlMs = options.persistenceTtlMs;
|
|
440
472
|
if (options.serveStaleOnOffline !== undefined)
|
|
441
473
|
cacheServeStaleOnOffline = options.serveStaleOnOffline;
|
|
474
|
+
if (options.clearOnPageLoad !== undefined)
|
|
475
|
+
cacheClearOnPageLoad = options.clearOnPageLoad;
|
|
442
476
|
}
|
|
443
477
|
/**
|
|
444
478
|
* Manually invalidate entries in the SDK's GET cache.
|
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.8 | Generated: 2026-02-22T11:38:27.387Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -120,8 +120,9 @@ Returns true if the SDK currently has any auth credential set (bearer token or A
|
|
|
120
120
|
persistence?: 'none' | 'indexeddb'
|
|
121
121
|
persistenceTtlMs?: number
|
|
122
122
|
serveStaleOnOffline?: boolean
|
|
123
|
+
clearOnPageLoad?: boolean
|
|
123
124
|
}) → `void`
|
|
124
|
-
Configure the SDK's built-in in-memory GET cache. The cache is transparent — it sits inside the HTTP layer and requires no changes to your existing API calls. All GET requests benefit automatically. Per-resource rules (collections/products → 1 h, proofs → 30 s, etc.) override this value. in-memory only (`'none'`, default). Ignored in Node.js. fallback, from the original fetch time (default: 7 days). `SmartlinksOfflineError` with stale data instead of propagating the network error. ```ts // Enable IndexedDB persistence for offline support configureSdkCache({ persistence: 'indexeddb' }) // Disable cache entirely in test environments configureSdkCache({ enabled: false }) ```
|
|
125
|
+
Configure the SDK's built-in in-memory GET cache. The cache is transparent — it sits inside the HTTP layer and requires no changes to your existing API calls. All GET requests benefit automatically. Per-resource rules (collections/products → 1 h, proofs → 30 s, etc.) override this value. in-memory only (`'none'`, default). Ignored in Node.js. fallback, from the original fetch time (default: 7 days). `SmartlinksOfflineError` with stale data instead of propagating the network error. caches on page load/refresh. IndexedDB persists for offline. ```ts // Enable IndexedDB persistence for offline support configureSdkCache({ persistence: 'indexeddb' }) // Disable cache entirely in test environments configureSdkCache({ enabled: false }) // Keep caches across page refreshes (not recommended for production) configureSdkCache({ clearOnPageLoad: false }) ```
|
|
125
126
|
|
|
126
127
|
**invalidateCache**(urlPattern?: string) → `void`
|
|
127
128
|
Manually invalidate entries in the SDK's GET cache. *contains* this string is removed. Omit (or pass `undefined`) to wipe the entire cache. ```ts invalidateCache() // clear everything invalidateCache('/collection/abc123') // one specific collection invalidateCache('/product/') // all product responses ```
|
package/docs/caching.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Caching Strategy
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Smartlinks SDK implements a multi-tier caching system designed to balance performance with data freshness:
|
|
6
|
+
|
|
7
|
+
1. **In-Memory Cache (L1)** - Fastest, cleared on page refresh
|
|
8
|
+
2. **SessionStorage** - Cleared on page refresh (not tab close)
|
|
9
|
+
3. **IndexedDB (L2)** - Persists across refreshes for offline support
|
|
10
|
+
|
|
11
|
+
## Cache Behavior on Page Refresh
|
|
12
|
+
|
|
13
|
+
### Default Behavior (Recommended)
|
|
14
|
+
|
|
15
|
+
When you refresh the page (F5, Ctrl+F5, or window reload):
|
|
16
|
+
|
|
17
|
+
- ✅ **In-memory cache** - Automatically cleared (module re-initialization)
|
|
18
|
+
- ✅ **SessionStorage** - Explicitly cleared on page load
|
|
19
|
+
- ✅ **IndexedDB** - Preserved for offline fallback only
|
|
20
|
+
|
|
21
|
+
This ensures that **page refreshes always fetch fresh data from the server**, while maintaining offline capabilities.
|
|
22
|
+
|
|
23
|
+
### How It Works
|
|
24
|
+
|
|
25
|
+
1. **During browsing (same page load)**:
|
|
26
|
+
- API requests are cached in memory and sessionStorage
|
|
27
|
+
- Subsequent identical requests serve from cache (fast!)
|
|
28
|
+
- TTL rules determine cache freshness (30s - 1h depending on resource)
|
|
29
|
+
|
|
30
|
+
2. **On page refresh**:
|
|
31
|
+
- Module re-initialization clears in-memory cache
|
|
32
|
+
- `clearSessionCacheOnLoad()` runs automatically
|
|
33
|
+
- SessionStorage caches are removed
|
|
34
|
+
- Fresh network requests are made
|
|
35
|
+
|
|
36
|
+
3. **When offline** (with `persistence: 'indexeddb'`):
|
|
37
|
+
- Network request fails
|
|
38
|
+
- SDK checks IndexedDB for stale data
|
|
39
|
+
- If found and within 7-day TTL, throws `SmartlinksOfflineError` with data
|
|
40
|
+
- App can catch this and use stale data gracefully
|
|
41
|
+
|
|
42
|
+
## Cache Configuration
|
|
43
|
+
|
|
44
|
+
### Enable IndexedDB Persistence
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { configureSdkCache } from '@smartlinks/sdk';
|
|
48
|
+
|
|
49
|
+
// Enable offline support via IndexedDB
|
|
50
|
+
configureSdkCache({
|
|
51
|
+
persistence: 'indexeddb',
|
|
52
|
+
persistenceTtlMs: 7 * 24 * 60 * 60_000, // 7 days
|
|
53
|
+
serveStaleOnOffline: true,
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Disable Clear-on-Refresh (Not Recommended)
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Keep caches across page refreshes
|
|
61
|
+
// ⚠️ Not recommended for production - you'll serve stale data after refresh
|
|
62
|
+
configureSdkCache({
|
|
63
|
+
clearOnPageLoad: false,
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Disable Caching Entirely
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Useful for testing or debugging
|
|
71
|
+
configureSdkCache({
|
|
72
|
+
enabled: false,
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Cache Storage Types
|
|
77
|
+
|
|
78
|
+
### `storage: 'memory'` (Default)
|
|
79
|
+
- Lives in JavaScript memory (Map)
|
|
80
|
+
- Cleared on page refresh
|
|
81
|
+
- Fastest access
|
|
82
|
+
- No quota limits
|
|
83
|
+
|
|
84
|
+
### `storage: 'session'`
|
|
85
|
+
- Lives in `sessionStorage`
|
|
86
|
+
- **Cleared on page load** (new behavior)
|
|
87
|
+
- Survives navigation within same session
|
|
88
|
+
- ~5-10MB quota
|
|
89
|
+
|
|
90
|
+
### `storage: 'local'`
|
|
91
|
+
- Lives in `localStorage`
|
|
92
|
+
- Persists across browser restarts
|
|
93
|
+
- Use sparingly for truly persistent data
|
|
94
|
+
- ~5-10MB quota
|
|
95
|
+
|
|
96
|
+
### `persistence: 'indexeddb'`
|
|
97
|
+
- Lives in IndexedDB (L2 cache)
|
|
98
|
+
- Persists across refreshes and restarts
|
|
99
|
+
- **Only used as offline fallback**, not for normal requests
|
|
100
|
+
- ~50MB+ quota
|
|
101
|
+
|
|
102
|
+
## TTL Rules
|
|
103
|
+
|
|
104
|
+
Different API resources have different cache lifetimes:
|
|
105
|
+
|
|
106
|
+
| Resource | TTL | Reason |
|
|
107
|
+
|----------|-----|--------|
|
|
108
|
+
| `/proof/*` | 30 seconds | Proofs change frequently |
|
|
109
|
+
| `/attestation/*` | 2 minutes | Attestations update regularly |
|
|
110
|
+
| `/product/*` | 1 hour | Products are relatively stable |
|
|
111
|
+
| `/variant/*` | 1 hour | Variants rarely change |
|
|
112
|
+
| `/collection/*` | 1 hour | Collections are stable |
|
|
113
|
+
| Everything else | 1 minute | Default safety |
|
|
114
|
+
|
|
115
|
+
## Manual Cache Control
|
|
116
|
+
|
|
117
|
+
### Invalidate Specific Resource
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { invalidateCache } from '@smartlinks/sdk';
|
|
121
|
+
|
|
122
|
+
// Clear cache for a specific collection
|
|
123
|
+
invalidateCache('/collection/abc123');
|
|
124
|
+
|
|
125
|
+
// Clear all product caches
|
|
126
|
+
invalidateCache('/product/');
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Clear All Caches
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { invalidateCache } from '@smartlinks/sdk';
|
|
133
|
+
|
|
134
|
+
// Nuclear option - clear everything
|
|
135
|
+
invalidateCache();
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Best Practices
|
|
139
|
+
|
|
140
|
+
1. **Default is best** - The SDK defaults are designed for optimal behavior
|
|
141
|
+
2. **Enable IndexedDB for PWAs** - If building offline-first apps
|
|
142
|
+
3. **Don't disable clearOnPageLoad** - Users expect fresh data after refresh
|
|
143
|
+
4. **Use invalidateCache() after mutations** - SDK does this automatically for you
|
|
144
|
+
5. **Trust the TTL rules** - They're tuned for smartlinks.app API behavior
|
|
145
|
+
|
|
146
|
+
## Offline Mode Example
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { collection, SmartlinksOfflineError } from '@smartlinks/sdk';
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const data = await collection.get('abc123');
|
|
153
|
+
// Fresh data from server
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof SmartlinksOfflineError) {
|
|
156
|
+
// Network failed, but we have stale data
|
|
157
|
+
console.warn('Offline - using stale data from', error.cachedAt);
|
|
158
|
+
const staleData = error.data;
|
|
159
|
+
// Display stale data with a banner
|
|
160
|
+
} else {
|
|
161
|
+
// Real error - no offline data available
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Migration from Old Behavior
|
|
168
|
+
|
|
169
|
+
If you previously relied on caches persisting across refreshes:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// Old (implicit) behavior:
|
|
173
|
+
// - SessionStorage survived refreshes
|
|
174
|
+
// - Apps might see stale data after F5
|
|
175
|
+
|
|
176
|
+
// New (explicit) behavior:
|
|
177
|
+
configureSdkCache({
|
|
178
|
+
clearOnPageLoad: true, // default - fresh data on refresh
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// If you REALLY need old behavior (not recommended):
|
|
182
|
+
configureSdkCache({
|
|
183
|
+
clearOnPageLoad: false,
|
|
184
|
+
persistence: 'indexeddb', // move to IndexedDB instead
|
|
185
|
+
});
|
|
186
|
+
```
|
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
|