@proveanything/smartlinks 1.3.46 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/auth.d.ts +6 -0
- package/dist/api/auth.js +11 -1
- package/dist/docs/API_SUMMARY.md +20 -3
- package/dist/docs/ai-guide-template.md +241 -0
- package/dist/http.d.ts +75 -2
- package/dist/http.js +439 -85
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/persistentCache.d.ts +36 -0
- package/dist/persistentCache.js +178 -0
- package/dist/types/error.d.ts +36 -0
- package/dist/types/error.js +42 -0
- package/docs/API_SUMMARY.md +20 -3
- package/docs/ai-guide-template.md +241 -0
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface PersistentCacheEntry {
|
|
2
|
+
/** The cached response data. */
|
|
3
|
+
data: any;
|
|
4
|
+
/** Unix ms timestamp of when the data was originally fetched from the network. */
|
|
5
|
+
timestamp: number;
|
|
6
|
+
/** Unix ms timestamp of when this entry was written to IndexedDB. */
|
|
7
|
+
persistedAt: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Returns true only in environments where IndexedDB is genuinely available.
|
|
11
|
+
* False in Node.js, and in some private-browsing contexts that stub indexedDB
|
|
12
|
+
* but throw on open.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isIdbAvailable(): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Read an entry from IndexedDB.
|
|
17
|
+
* Returns `null` when IDB is unavailable, the key doesn't exist, or on any error.
|
|
18
|
+
* Safe to call in Node.js — returns null immediately.
|
|
19
|
+
*/
|
|
20
|
+
export declare function idbGet(key: string): Promise<PersistentCacheEntry | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Write an entry to IndexedDB.
|
|
23
|
+
* Fails silently on quota exceeded, private browsing, or any other error.
|
|
24
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
25
|
+
*/
|
|
26
|
+
export declare function idbSet(key: string, entry: PersistentCacheEntry): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Delete a single entry from IndexedDB.
|
|
29
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
30
|
+
*/
|
|
31
|
+
export declare function idbDelete(key: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Clear all IDB entries, or only those whose key contains `pattern`.
|
|
34
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
35
|
+
*/
|
|
36
|
+
export declare function idbClear(pattern?: string): Promise<void>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/persistentCache.ts
|
|
2
|
+
// IndexedDB-backed L2 cache for the SDK's HTTP layer.
|
|
3
|
+
//
|
|
4
|
+
// Every exported function is Node-safe: all IDB access is guarded by
|
|
5
|
+
// isIdbAvailable() and wrapped in try/catch so that failures in private-
|
|
6
|
+
// browsing, quota-exceeded situations, or server-side environments are always
|
|
7
|
+
// silent — they never propagate errors to the caller.
|
|
8
|
+
const DB_NAME = 'smartlinks-sdk-cache';
|
|
9
|
+
const STORE_NAME = 'responses';
|
|
10
|
+
const DB_VERSION = 1;
|
|
11
|
+
let dbPromise = null;
|
|
12
|
+
/**
|
|
13
|
+
* Returns true only in environments where IndexedDB is genuinely available.
|
|
14
|
+
* False in Node.js, and in some private-browsing contexts that stub indexedDB
|
|
15
|
+
* but throw on open.
|
|
16
|
+
*/
|
|
17
|
+
export function isIdbAvailable() {
|
|
18
|
+
try {
|
|
19
|
+
return typeof indexedDB !== 'undefined' && indexedDB !== null;
|
|
20
|
+
}
|
|
21
|
+
catch (_a) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function openDb() {
|
|
26
|
+
if (dbPromise)
|
|
27
|
+
return dbPromise;
|
|
28
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
29
|
+
try {
|
|
30
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
31
|
+
req.onupgradeneeded = (event) => {
|
|
32
|
+
const db = event.target.result;
|
|
33
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
34
|
+
db.createObjectStore(STORE_NAME);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
req.onsuccess = (event) => {
|
|
38
|
+
resolve(event.target.result);
|
|
39
|
+
};
|
|
40
|
+
req.onerror = () => {
|
|
41
|
+
dbPromise = null;
|
|
42
|
+
reject(req.error);
|
|
43
|
+
};
|
|
44
|
+
req.onblocked = () => {
|
|
45
|
+
dbPromise = null;
|
|
46
|
+
reject(new Error('[smartlinks] IndexedDB open blocked'));
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
dbPromise = null;
|
|
51
|
+
reject(err);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return dbPromise;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read an entry from IndexedDB.
|
|
58
|
+
* Returns `null` when IDB is unavailable, the key doesn't exist, or on any error.
|
|
59
|
+
* Safe to call in Node.js — returns null immediately.
|
|
60
|
+
*/
|
|
61
|
+
export async function idbGet(key) {
|
|
62
|
+
if (!isIdbAvailable())
|
|
63
|
+
return null;
|
|
64
|
+
try {
|
|
65
|
+
const db = await openDb();
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
try {
|
|
68
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
69
|
+
const req = tx.objectStore(STORE_NAME).get(key);
|
|
70
|
+
req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
|
|
71
|
+
req.onerror = () => resolve(null);
|
|
72
|
+
}
|
|
73
|
+
catch (_a) {
|
|
74
|
+
resolve(null);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (_a) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Write an entry to IndexedDB.
|
|
84
|
+
* Fails silently on quota exceeded, private browsing, or any other error.
|
|
85
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
86
|
+
*/
|
|
87
|
+
export async function idbSet(key, entry) {
|
|
88
|
+
if (!isIdbAvailable())
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
const db = await openDb();
|
|
92
|
+
await new Promise((resolve) => {
|
|
93
|
+
try {
|
|
94
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
95
|
+
tx.objectStore(STORE_NAME).put(entry, key);
|
|
96
|
+
tx.oncomplete = () => resolve();
|
|
97
|
+
tx.onerror = () => resolve(); // quota exceeded etc — fail silently
|
|
98
|
+
tx.onabort = () => resolve();
|
|
99
|
+
}
|
|
100
|
+
catch (_a) {
|
|
101
|
+
resolve();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (_a) {
|
|
106
|
+
// Fail silently — IDB persistence is best-effort
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Delete a single entry from IndexedDB.
|
|
111
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
112
|
+
*/
|
|
113
|
+
export async function idbDelete(key) {
|
|
114
|
+
if (!isIdbAvailable())
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
const db = await openDb();
|
|
118
|
+
await new Promise((resolve) => {
|
|
119
|
+
try {
|
|
120
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
121
|
+
tx.objectStore(STORE_NAME).delete(key);
|
|
122
|
+
tx.oncomplete = () => resolve();
|
|
123
|
+
tx.onerror = () => resolve();
|
|
124
|
+
tx.onabort = () => resolve();
|
|
125
|
+
}
|
|
126
|
+
catch (_a) {
|
|
127
|
+
resolve();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (_a) { }
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Clear all IDB entries, or only those whose key contains `pattern`.
|
|
135
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
136
|
+
*/
|
|
137
|
+
export async function idbClear(pattern) {
|
|
138
|
+
if (!isIdbAvailable())
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
const db = await openDb();
|
|
142
|
+
if (!pattern) {
|
|
143
|
+
await new Promise((resolve) => {
|
|
144
|
+
try {
|
|
145
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
146
|
+
tx.objectStore(STORE_NAME).clear();
|
|
147
|
+
tx.oncomplete = () => resolve();
|
|
148
|
+
tx.onerror = () => resolve();
|
|
149
|
+
tx.onabort = () => resolve();
|
|
150
|
+
}
|
|
151
|
+
catch (_a) {
|
|
152
|
+
resolve();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Pattern-based: enumerate all keys and delete matching ones.
|
|
158
|
+
await new Promise((resolve) => {
|
|
159
|
+
try {
|
|
160
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
161
|
+
const store = tx.objectStore(STORE_NAME);
|
|
162
|
+
const req = store.getAllKeys();
|
|
163
|
+
req.onsuccess = () => {
|
|
164
|
+
for (const key of req.result) {
|
|
165
|
+
if (key.includes(pattern))
|
|
166
|
+
store.delete(key);
|
|
167
|
+
}
|
|
168
|
+
resolve();
|
|
169
|
+
};
|
|
170
|
+
req.onerror = () => resolve();
|
|
171
|
+
}
|
|
172
|
+
catch (_a) {
|
|
173
|
+
resolve();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (_a) { }
|
|
178
|
+
}
|
package/dist/types/error.d.ts
CHANGED
|
@@ -73,3 +73,39 @@ export declare class SmartlinksApiError extends Error {
|
|
|
73
73
|
*/
|
|
74
74
|
toJSON(): Record<string, any>;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Thrown when a GET request fails due to network unavailability AND the
|
|
78
|
+
* persistent cache (IndexedDB) has a previously stored response for that
|
|
79
|
+
* resource. The `staleData` property contains the cached payload so the
|
|
80
|
+
* application can render in a degraded/offline state.
|
|
81
|
+
*
|
|
82
|
+
* Only thrown when persistence is enabled:
|
|
83
|
+
* `configureSdkCache({ persistence: 'indexeddb' })`
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { SmartlinksOfflineError } from '@proveanything/smartlinks'
|
|
88
|
+
*
|
|
89
|
+
* try {
|
|
90
|
+
* const data = await collection.get('abc123')
|
|
91
|
+
* } catch (err) {
|
|
92
|
+
* if (err instanceof SmartlinksOfflineError) {
|
|
93
|
+
* showOfflineBanner()
|
|
94
|
+
* renderWithData(err.staleData) // use the cached payload
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare class SmartlinksOfflineError extends Error {
|
|
100
|
+
/** The stale cached payload available when the network request failed. */
|
|
101
|
+
readonly staleData: any;
|
|
102
|
+
/** Unix ms timestamp of when the stale data was originally fetched from the server. */
|
|
103
|
+
readonly cachedAt: number;
|
|
104
|
+
constructor(message: string,
|
|
105
|
+
/** The stale cached payload available when the network request failed. */
|
|
106
|
+
staleData: any,
|
|
107
|
+
/** Unix ms timestamp of when the stale data was originally fetched from the server. */
|
|
108
|
+
cachedAt: number);
|
|
109
|
+
/** Age of the stale data in milliseconds at the time this error was thrown. */
|
|
110
|
+
get staleAgeMs(): number;
|
|
111
|
+
}
|
package/dist/types/error.js
CHANGED
|
@@ -96,3 +96,45 @@ export class SmartlinksApiError extends Error {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Thrown when a GET request fails due to network unavailability AND the
|
|
101
|
+
* persistent cache (IndexedDB) has a previously stored response for that
|
|
102
|
+
* resource. The `staleData` property contains the cached payload so the
|
|
103
|
+
* application can render in a degraded/offline state.
|
|
104
|
+
*
|
|
105
|
+
* Only thrown when persistence is enabled:
|
|
106
|
+
* `configureSdkCache({ persistence: 'indexeddb' })`
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* import { SmartlinksOfflineError } from '@proveanything/smartlinks'
|
|
111
|
+
*
|
|
112
|
+
* try {
|
|
113
|
+
* const data = await collection.get('abc123')
|
|
114
|
+
* } catch (err) {
|
|
115
|
+
* if (err instanceof SmartlinksOfflineError) {
|
|
116
|
+
* showOfflineBanner()
|
|
117
|
+
* renderWithData(err.staleData) // use the cached payload
|
|
118
|
+
* }
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export class SmartlinksOfflineError extends Error {
|
|
123
|
+
constructor(message,
|
|
124
|
+
/** The stale cached payload available when the network request failed. */
|
|
125
|
+
staleData,
|
|
126
|
+
/** Unix ms timestamp of when the stale data was originally fetched from the server. */
|
|
127
|
+
cachedAt) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.staleData = staleData;
|
|
130
|
+
this.cachedAt = cachedAt;
|
|
131
|
+
this.name = 'SmartlinksOfflineError';
|
|
132
|
+
if (Error.captureStackTrace) {
|
|
133
|
+
Error.captureStackTrace(this, SmartlinksOfflineError);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Age of the stale data in milliseconds at the time this error was thrown. */
|
|
137
|
+
get staleAgeMs() {
|
|
138
|
+
return Date.now() - this.cachedAt;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1 | Generated: 2026-02-20T19:32:34.980Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -18,6 +18,7 @@ For detailed guides on specific features:
|
|
|
18
18
|
- **[Theme Defaults](theme-defaults.md)** - Default theme values and presets
|
|
19
19
|
- **[Proof Claiming Methods](proof-claiming-methods.md)** - All methods for claiming/registering product ownership (NFC tags, serial numbers, auto-generated claims)
|
|
20
20
|
- **[App Data Storage](app-data-storage.md)** - User-specific and collection-scoped app data storage
|
|
21
|
+
- **[AI Guide Template](ai-guide-template.md)** - A sample for an app on how to build an AI setup guide
|
|
21
22
|
|
|
22
23
|
## API Namespaces
|
|
23
24
|
|
|
@@ -108,13 +109,29 @@ Get the currently configured API base URL. Returns null if initializeApi() has n
|
|
|
108
109
|
**isInitialized**() → `boolean`
|
|
109
110
|
Returns true if initializeApi() has been called at least once. Useful for guards in widgets or shared modules that want to skip initialization when another module has already done it. ```ts if (!isInitialized()) { initializeApi({ baseURL: 'https://smartlinks.app/api/v1' }) } ```
|
|
110
111
|
|
|
112
|
+
**hasAuthCredentials**() → `boolean`
|
|
113
|
+
Returns true if the SDK currently has any auth credential set (bearer token or API key). Use this as a cheap pre-flight check before calling endpoints that require authentication, to avoid issuing a network request that you already know will return a 401. ```ts if (hasAuthCredentials()) { const account = await auth.getAccount() } ```
|
|
114
|
+
|
|
115
|
+
**configureSdkCache**(options: {
|
|
116
|
+
enabled?: boolean
|
|
117
|
+
ttlMs?: number
|
|
118
|
+
maxEntries?: number
|
|
119
|
+
persistence?: 'none' | 'indexeddb'
|
|
120
|
+
persistenceTtlMs?: number
|
|
121
|
+
serveStaleOnOffline?: boolean
|
|
122
|
+
}) → `void`
|
|
123
|
+
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 }) ```
|
|
124
|
+
|
|
125
|
+
**invalidateCache**(urlPattern?: string) → `void`
|
|
126
|
+
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 ```
|
|
127
|
+
|
|
111
128
|
**proxyUploadFormData**(path: string,
|
|
112
129
|
formData: FormData,
|
|
113
130
|
onProgress?: (percent: number) → `void`
|
|
114
131
|
Upload a FormData payload via proxy with progress events using chunked postMessage. Parent is expected to implement the counterpart protocol.
|
|
115
132
|
|
|
116
133
|
**request**(path: string) → `Promise<T>`
|
|
117
|
-
Internal helper that performs a GET request to
|
|
134
|
+
Internal helper that performs a GET request to `${baseURL}${path}`, injecting headers for apiKey or bearerToken if present. Cache pipeline (when caching is not skipped): L1 hit → return from memory (no I/O) L2 hit → return from IndexedDB, promote to L1 (no network) Miss → fetch from network, store in L1 + L2 Offline → serve stale L2 entry via SmartlinksOfflineError (if persistence enabled) Concurrent identical GETs share one in-flight promise (deduplication). Node-safe: IndexedDB calls are no-ops when IDB is unavailable.
|
|
118
135
|
|
|
119
136
|
**post**(path: string,
|
|
120
137
|
body: any,
|
|
@@ -4128,7 +4145,7 @@ Tries to register a new user account. Can return a bearer token, or a Firebase t
|
|
|
4128
4145
|
Admin: Get a user bearer token (impersonation/automation). POST /admin/auth/userToken All fields are optional; at least one identifier should be provided.
|
|
4129
4146
|
|
|
4130
4147
|
**getAccount**() → `Promise<AccountInfoResponse>`
|
|
4131
|
-
Gets current account information for the logged in user. Returns user, owner, account, and location objects.
|
|
4148
|
+
Gets current account information for the logged in user. Returns user, owner, account, and location objects. Short-circuits immediately (no network request) when the SDK has no bearer token or API key set — the server would return 401 anyway. Throws a `SmartlinksApiError` with `statusCode 401` and `details.local = true` so callers can distinguish "never authenticated" from an actual server-side token rejection.
|
|
4132
4149
|
|
|
4133
4150
|
### authKit
|
|
4134
4151
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# AI Guide: Example App
|
|
2
|
+
|
|
3
|
+
<!-- CUSTOMIZE: Replace "Example App" and all content below with your app's specifics. -->
|
|
4
|
+
<!-- This file is consumed by AI orchestrators alongside public/app.manifest.json. -->
|
|
5
|
+
<!-- The manifest provides structured data; this file provides prose context and nuance. -->
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose & Identity
|
|
10
|
+
|
|
11
|
+
<!-- CUSTOMIZE: Describe what your app does in 2-3 sentences. -->
|
|
12
|
+
|
|
13
|
+
The **Example App** is a template SmartLinks microapp that demonstrates all platform patterns including setup wizards, data imports, widget embedding, and analytics. It serves as a reference implementation for app authors building on the SmartLinks platform.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Setup Wizard Instructions
|
|
18
|
+
|
|
19
|
+
An AI orchestrator should follow these steps to configure the Example App for a collection. The structured questions and schema are in `app.manifest.json` under `setup`.
|
|
20
|
+
|
|
21
|
+
### Prerequisites
|
|
22
|
+
|
|
23
|
+
- The user must have **admin access** to the target collection.
|
|
24
|
+
- The `collectionId` and `appId` must be known (typically from URL context).
|
|
25
|
+
|
|
26
|
+
### Questions to Ask
|
|
27
|
+
|
|
28
|
+
Walk the user through each question defined in `setup.questions[]` of the manifest:
|
|
29
|
+
|
|
30
|
+
1. **App Title** (`appTitle`): Ask what the app should be called. Offer to auto-generate a suggestion using `SL.ai.chat.completions` if the user wants help (see `contentHints` in the manifest).
|
|
31
|
+
2. **Enable Notifications** (`enableNotifications`): Ask whether the app should send notifications to users.
|
|
32
|
+
|
|
33
|
+
<!-- CUSTOMIZE: Add app-specific guidance for each question. For example: -->
|
|
34
|
+
<!-- "If the user is setting up a competition, suggest a title like 'Win a [Product Name]!'" -->
|
|
35
|
+
<!-- "For plant passports, notifications are typically disabled." -->
|
|
36
|
+
|
|
37
|
+
### How to Save the Config
|
|
38
|
+
|
|
39
|
+
After collecting answers, validate against `setup.configSchema` and save:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
await SL.appConfiguration.setConfig({
|
|
43
|
+
collectionId,
|
|
44
|
+
appId,
|
|
45
|
+
config: {
|
|
46
|
+
appTitle: "User's chosen title",
|
|
47
|
+
enableNotifications: false
|
|
48
|
+
},
|
|
49
|
+
admin: true // REQUIRED for admin operations
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Post-Setup Verification
|
|
54
|
+
|
|
55
|
+
After saving, confirm by reading the config back:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const saved = await SL.appConfiguration.getConfig({
|
|
59
|
+
collectionId,
|
|
60
|
+
appId,
|
|
61
|
+
admin: true
|
|
62
|
+
});
|
|
63
|
+
// Verify saved.appTitle and saved.enableNotifications match expectations
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Tell the user: "Your app is configured! You can adjust settings anytime by asking me to update the configuration."
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Import Instructions
|
|
71
|
+
|
|
72
|
+
The app supports bulk import of product-level configuration. The field definitions are in `app.manifest.json` under `import`.
|
|
73
|
+
|
|
74
|
+
### CSV Template
|
|
75
|
+
|
|
76
|
+
Generate or share this template with the user:
|
|
77
|
+
|
|
78
|
+
```csv
|
|
79
|
+
productId,appTitle,enableNotifications
|
|
80
|
+
prod_001,My First Product,true
|
|
81
|
+
prod_002,My Second Product,false
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
<!-- CUSTOMIZE: Add real-world examples relevant to your app. -->
|
|
85
|
+
|
|
86
|
+
### Field Validation Rules
|
|
87
|
+
|
|
88
|
+
| Field | Type | Required | Validation |
|
|
89
|
+
| --------------------- | ------- | -------- | ------------------------------------- |
|
|
90
|
+
| `productId` | string | ✅ | Must be a valid SmartLinks product ID |
|
|
91
|
+
| `appTitle` | string | ✅ | Non-empty string |
|
|
92
|
+
| `enableNotifications` | boolean | ❌ | Defaults to `false` |
|
|
93
|
+
|
|
94
|
+
<!-- CUSTOMIZE: Add app-specific validation rules. For example: -->
|
|
95
|
+
<!-- "For plant passports: botanicalName must be in Latin binomial format." -->
|
|
96
|
+
|
|
97
|
+
### API Call Sequence
|
|
98
|
+
|
|
99
|
+
For each row in the CSV:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
await SL.appConfiguration.setConfig({
|
|
103
|
+
collectionId,
|
|
104
|
+
productId: row.productId, // From CSV
|
|
105
|
+
appId,
|
|
106
|
+
config: {
|
|
107
|
+
appTitle: row.appTitle,
|
|
108
|
+
enableNotifications: row.enableNotifications ?? false
|
|
109
|
+
},
|
|
110
|
+
admin: true
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Error Handling
|
|
115
|
+
|
|
116
|
+
- If a `productId` is invalid, log the error and continue with the next row.
|
|
117
|
+
- After processing all rows, report a summary: `"Imported X of Y products successfully. Z failed."`.
|
|
118
|
+
- For failed rows, list the product ID and error message so the user can fix the data.
|
|
119
|
+
|
|
120
|
+
### Cross-App Import
|
|
121
|
+
|
|
122
|
+
When importing data for multiple apps simultaneously:
|
|
123
|
+
|
|
124
|
+
1. Fetch `app.manifest.json` from each app.
|
|
125
|
+
2. Merge `import.fields` arrays (prefix field names with app name if there are conflicts).
|
|
126
|
+
3. Generate a combined CSV template.
|
|
127
|
+
4. For each row, split into separate payloads per app and call each app's `saveWith.method`.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Widget Embedding Guide
|
|
132
|
+
|
|
133
|
+
### Available Widgets
|
|
134
|
+
|
|
135
|
+
| Widget | Description | Sizes |
|
|
136
|
+
| --------------- | ------------------------------------------ | ------------------------ |
|
|
137
|
+
| `ExampleWidget` | Demo widget showing SmartLinks integration | compact, standard, large |
|
|
138
|
+
|
|
139
|
+
<!-- CUSTOMIZE: List all widgets your app exports. -->
|
|
140
|
+
|
|
141
|
+
### Props Reference
|
|
142
|
+
|
|
143
|
+
All widgets receive `SmartLinksWidgetProps`:
|
|
144
|
+
|
|
145
|
+
| Prop | Type | Required | Description |
|
|
146
|
+
| ----------------- | -------------- | -------- | --------------------------------------- |
|
|
147
|
+
| `collectionId` | string | ✅ | Collection context |
|
|
148
|
+
| `appId` | string | ✅ | App identifier |
|
|
149
|
+
| `SL` | SmartLinks SDK | ✅ | Pre-initialized SDK instance |
|
|
150
|
+
| `productId` | string | ❌ | Product context |
|
|
151
|
+
| `proofId` | string | ❌ | Proof context |
|
|
152
|
+
| `user` | object | ❌ | Current user info |
|
|
153
|
+
| `onNavigate` | function | ❌ | Navigation callback |
|
|
154
|
+
| `publicPortalUrl` | string | ❌ | URL to full app for deep linking |
|
|
155
|
+
| `size` | string | ❌ | `"compact"`, `"standard"`, or `"large"` |
|
|
156
|
+
| `lang` | string | ❌ | Language code (e.g., `"en"`) |
|
|
157
|
+
| `translations` | object | ❌ | Translation overrides |
|
|
158
|
+
|
|
159
|
+
### Example Code
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { ExampleWidget } from '@my-app/widgets';
|
|
163
|
+
|
|
164
|
+
<ExampleWidget
|
|
165
|
+
collectionId="col_123"
|
|
166
|
+
appId="example-app"
|
|
167
|
+
SL={SL}
|
|
168
|
+
size="standard"
|
|
169
|
+
onNavigate={(path) => router.push(path)}
|
|
170
|
+
/>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
<!-- CUSTOMIZE: Show real widget usage with app-specific props. -->
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Tunable Settings
|
|
178
|
+
|
|
179
|
+
These settings can be adjusted after initial setup without reconfiguring everything. The AI can modify them in response to user requests like "turn off notifications" or optimization suggestions.
|
|
180
|
+
|
|
181
|
+
| Setting | Type | Description |
|
|
182
|
+
| --------------------- | ------- | ------------------------------ |
|
|
183
|
+
| `enableNotifications` | boolean | Toggle notifications on or off |
|
|
184
|
+
|
|
185
|
+
<!-- CUSTOMIZE: List all tunable fields with guidance on when to change them. -->
|
|
186
|
+
|
|
187
|
+
To update a tunable setting:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// 1. Read current config
|
|
191
|
+
const current = await SL.appConfiguration.getConfig({ collectionId, appId, admin: true });
|
|
192
|
+
|
|
193
|
+
// 2. Merge the change
|
|
194
|
+
await SL.appConfiguration.setConfig({
|
|
195
|
+
collectionId,
|
|
196
|
+
appId,
|
|
197
|
+
config: { ...current, enableNotifications: true },
|
|
198
|
+
admin: true
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Metrics & Analytics
|
|
205
|
+
|
|
206
|
+
### Tracked Interactions
|
|
207
|
+
|
|
208
|
+
| Interaction ID | Description |
|
|
209
|
+
| -------------- | --------------------------------------------- |
|
|
210
|
+
| `page-view` | Tracks each time a user views the public page |
|
|
211
|
+
|
|
212
|
+
<!-- CUSTOMIZE: List all interaction IDs your app tracks. -->
|
|
213
|
+
|
|
214
|
+
### KPIs
|
|
215
|
+
|
|
216
|
+
| KPI | How to Compute |
|
|
217
|
+
| ----------- | -------------------------------------------------------------------------------------- |
|
|
218
|
+
| Total Views | `SL.interactions.countsByOutcome(collectionId, { appId, interactionId: 'page-view' })` |
|
|
219
|
+
|
|
220
|
+
<!-- CUSTOMIZE: Add app-specific KPIs and interpretation guidance. -->
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Troubleshooting
|
|
225
|
+
|
|
226
|
+
### Common Issues
|
|
227
|
+
|
|
228
|
+
| Issue | Cause | Fix |
|
|
229
|
+
| --------------------- | -------------------------- | ----------------------------------------------------- |
|
|
230
|
+
| Config save fails | Missing `admin: true` flag | Always include `admin: true` for admin operations |
|
|
231
|
+
| Widget doesn't render | Missing required props | Ensure `collectionId`, `appId`, and `SL` are provided |
|
|
232
|
+
| Import skips rows | Invalid `productId` | Verify product IDs exist in the collection |
|
|
233
|
+
| Theme not applied | Missing `?theme=` param | Check URL parameters or postMessage setup |
|
|
234
|
+
|
|
235
|
+
<!-- CUSTOMIZE: Add app-specific troubleshooting entries. -->
|
|
236
|
+
|
|
237
|
+
### Getting Help
|
|
238
|
+
|
|
239
|
+
- **SDK Docs**: `node_modules/@proveanything/smartlinks/docs/`
|
|
240
|
+
- **App Manifest**: `public/app.manifest.json`
|
|
241
|
+
- **Platform Guide**: `src/docs/smartlinks/about.md`
|