@proveanything/smartlinks 1.11.12 → 1.11.13
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/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/navigation.d.ts +24 -0
- package/dist/api/navigation.js +156 -0
- package/dist/docs/API_SUMMARY.md +58 -1
- package/dist/docs/deep-link-discovery.md +144 -115
- package/dist/openapi.yaml +22 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/navigation.d.ts +121 -0
- package/dist/types/navigation.js +1 -0
- package/docs/API_SUMMARY.md +58 -1
- package/docs/deep-link-discovery.md +144 -115
- package/openapi.yaml +22 -0
- package/package.json +1 -1
package/dist/api/index.d.ts
CHANGED
package/dist/api/index.js
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LinkTarget, ResolveLinkContext, ResolvedLink } from '../types/navigation';
|
|
2
|
+
export declare namespace navigation {
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a stored `LinkTarget` into an executable navigation action.
|
|
5
|
+
*
|
|
6
|
+
* The resolver handles the embedded/standalone distinction automatically:
|
|
7
|
+
*
|
|
8
|
+
* | Kind | Embedded (container/widget/iframe) | Standalone |
|
|
9
|
+
* |-------------------|---------------------------------------------------------------|-------------------|
|
|
10
|
+
* | `external _blank` | `window.open(url, '_blank', 'noopener,noreferrer')` | same |
|
|
11
|
+
* | `external _self` | `window.location.assign(url)` | same |
|
|
12
|
+
* | `app` / `deep` | `postMessage` to parent shell; shell appends context params | hash-route assign |
|
|
13
|
+
*
|
|
14
|
+
* Context params (`collectionId`, `productId`, `proofId`, etc.) are **not**
|
|
15
|
+
* included in the message payload — the parent SmartLinks shell owns them and
|
|
16
|
+
* appends them before broadcasting the navigation event.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const r = SL.navigation.resolveLink(config.ctaLink);
|
|
20
|
+
* r.navigate(); // perform navigation
|
|
21
|
+
* button.ariaLabel = r.describe(); // human-readable label
|
|
22
|
+
*/
|
|
23
|
+
function resolveLink(link: LinkTarget, ctx?: ResolveLinkContext): ResolvedLink;
|
|
24
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// src/api/navigation.ts
|
|
2
|
+
import { analytics } from './analytics';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Internal helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function detectEmbedded() {
|
|
7
|
+
if (typeof window === 'undefined')
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
return window.parent !== window;
|
|
11
|
+
}
|
|
12
|
+
catch (_a) {
|
|
13
|
+
// Cross-origin parent — access throws, which means we are embedded.
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Normalise a `deepLinkId` to a hash-route path.
|
|
19
|
+
* `deepLinkId` is either a bare path ("/winners") or a "path?params" string
|
|
20
|
+
* as minted from manifest entries. Strip any inline query string — params are
|
|
21
|
+
* forwarded separately.
|
|
22
|
+
*/
|
|
23
|
+
function deepPath(deepLinkId) {
|
|
24
|
+
const [path] = deepLinkId.split('?');
|
|
25
|
+
if (!path)
|
|
26
|
+
return '/';
|
|
27
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
28
|
+
}
|
|
29
|
+
function qs(params) {
|
|
30
|
+
if (!params)
|
|
31
|
+
return '';
|
|
32
|
+
const keys = Object.keys(params);
|
|
33
|
+
if (!keys.length)
|
|
34
|
+
return '';
|
|
35
|
+
const usp = new URLSearchParams();
|
|
36
|
+
for (const k of keys)
|
|
37
|
+
usp.set(k, params[k]);
|
|
38
|
+
return `?${usp.toString()}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the window-features string for `window.open`.
|
|
42
|
+
* `noopener` and `noreferrer` are always present; a caller-supplied `rel`
|
|
43
|
+
* can add extra features (e.g. `'popup'`) which are appended after the
|
|
44
|
+
* mandatory security flags.
|
|
45
|
+
*/
|
|
46
|
+
function windowFeatures(rel) {
|
|
47
|
+
const base = 'noopener,noreferrer';
|
|
48
|
+
if (!rel)
|
|
49
|
+
return base;
|
|
50
|
+
// Avoid duplicating the security flags if the caller already included them.
|
|
51
|
+
const extra = rel
|
|
52
|
+
.split(',')
|
|
53
|
+
.map(f => f.trim())
|
|
54
|
+
.filter(f => f && f !== 'noopener' && f !== 'noreferrer')
|
|
55
|
+
.join(',');
|
|
56
|
+
return extra ? `${base},${extra}` : base;
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Public namespace
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
export var navigation;
|
|
62
|
+
(function (navigation) {
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a stored `LinkTarget` into an executable navigation action.
|
|
65
|
+
*
|
|
66
|
+
* The resolver handles the embedded/standalone distinction automatically:
|
|
67
|
+
*
|
|
68
|
+
* | Kind | Embedded (container/widget/iframe) | Standalone |
|
|
69
|
+
* |-------------------|---------------------------------------------------------------|-------------------|
|
|
70
|
+
* | `external _blank` | `window.open(url, '_blank', 'noopener,noreferrer')` | same |
|
|
71
|
+
* | `external _self` | `window.location.assign(url)` | same |
|
|
72
|
+
* | `app` / `deep` | `postMessage` to parent shell; shell appends context params | hash-route assign |
|
|
73
|
+
*
|
|
74
|
+
* Context params (`collectionId`, `productId`, `proofId`, etc.) are **not**
|
|
75
|
+
* included in the message payload — the parent SmartLinks shell owns them and
|
|
76
|
+
* appends them before broadcasting the navigation event.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const r = SL.navigation.resolveLink(config.ctaLink);
|
|
80
|
+
* r.navigate(); // perform navigation
|
|
81
|
+
* button.ariaLabel = r.describe(); // human-readable label
|
|
82
|
+
*/
|
|
83
|
+
function resolveLink(link, ctx = {}) {
|
|
84
|
+
var _a, _b;
|
|
85
|
+
const win = (_a = ctx.win) !== null && _a !== void 0 ? _a : (typeof window !== 'undefined' ? window : undefined);
|
|
86
|
+
const embedded = (_b = ctx.embedded) !== null && _b !== void 0 ? _b : detectEmbedded();
|
|
87
|
+
function fireTracking(resolved) {
|
|
88
|
+
if (!ctx.track)
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
analytics.browser.trackLinkClick(Object.assign({
|
|
92
|
+
// Derived from the LinkTarget — caller overrides win if needed.
|
|
93
|
+
href: link.kind === 'external' ? link.url : '', isExternal: link.kind === 'external', destinationAppId: link.kind !== 'external' ? link.appId : undefined, linkTitle: resolved.describe() }, ctx.track));
|
|
94
|
+
}
|
|
95
|
+
catch (_a) {
|
|
96
|
+
// Tracking must never prevent navigation.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const resolved = {
|
|
100
|
+
navigate() {
|
|
101
|
+
var _a, _b, _c;
|
|
102
|
+
if (!win)
|
|
103
|
+
return;
|
|
104
|
+
fireTracking(resolved);
|
|
105
|
+
// External links are always handled the same way regardless of context.
|
|
106
|
+
if (link.kind === 'external') {
|
|
107
|
+
if (link.target === '_blank') {
|
|
108
|
+
win.open(link.url, '_blank', windowFeatures(link.rel));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
win.location.assign(link.url);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (embedded) {
|
|
116
|
+
// Post a navigation request to the parent SmartLinks shell. The shell
|
|
117
|
+
// will attach collectionId, productId, proofId, etc. before acting.
|
|
118
|
+
const postTarget = (_a = ctx.postTarget) !== null && _a !== void 0 ? _a : win.parent;
|
|
119
|
+
if (!postTarget)
|
|
120
|
+
return;
|
|
121
|
+
postTarget.postMessage({
|
|
122
|
+
type: 'smartlinks-navigate',
|
|
123
|
+
appId: link.appId,
|
|
124
|
+
path: link.kind === 'deep' ? deepPath(link.deepLinkId) : '/',
|
|
125
|
+
params: link.kind === 'deep' ? ((_b = link.params) !== null && _b !== void 0 ? _b : {}) : {},
|
|
126
|
+
target: (_c = link.target) !== null && _c !== void 0 ? _c : '_self',
|
|
127
|
+
}, '*');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Standalone fallback — construct a best-effort hash route. This path
|
|
131
|
+
// is intentionally minimal: the canonical URL shape is owned by the
|
|
132
|
+
// platform shell. This is only used when no parent shell is present
|
|
133
|
+
// (e.g. deep-linking from a server-rendered page).
|
|
134
|
+
const hash = link.kind === 'deep'
|
|
135
|
+
? `#${deepPath(link.deepLinkId)}${qs(link.params)}`
|
|
136
|
+
: `#/`;
|
|
137
|
+
const url = `${win.location.pathname}?appId=${encodeURIComponent(link.appId)}${hash}`;
|
|
138
|
+
if (link.target === '_blank') {
|
|
139
|
+
win.open(url, '_blank', windowFeatures());
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
win.location.assign(url);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
describe() {
|
|
146
|
+
if (link.kind === 'external')
|
|
147
|
+
return `Open ${link.url}`;
|
|
148
|
+
if (link.kind === 'app')
|
|
149
|
+
return `Open app ${link.appId}`;
|
|
150
|
+
return `Open ${link.appId} \u2192 ${link.deepLinkId}`;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
return resolved;
|
|
154
|
+
}
|
|
155
|
+
navigation.resolveLink = resolveLink;
|
|
156
|
+
})(navigation || (navigation = {}));
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.11.
|
|
3
|
+
Version: 1.11.13 | Generated: 2026-05-05T09:55:20.018Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -110,6 +110,7 @@ The Smartlinks SDK is organized into the following namespaces:
|
|
|
110
110
|
- **jobs** - Functions for jobs operations
|
|
111
111
|
- **journeysAnalytics** - Functions for journeysAnalytics operations
|
|
112
112
|
- **location** - Functions for location operations
|
|
113
|
+
- **navigation** - Functions for navigation operations
|
|
113
114
|
- **order** - Functions for order operations
|
|
114
115
|
- **products** - Functions for products operations
|
|
115
116
|
- **realtime** - Functions for realtime operations
|
|
@@ -5669,6 +5670,57 @@ interface RecordLoyaltyTransactionBody {
|
|
|
5669
5670
|
|
|
5670
5671
|
**DataBlock** = `Record<string, unknown>`
|
|
5671
5672
|
|
|
5673
|
+
### navigation
|
|
5674
|
+
|
|
5675
|
+
**ResolveLinkContext** (interface)
|
|
5676
|
+
```typescript
|
|
5677
|
+
interface ResolveLinkContext {
|
|
5678
|
+
* True when running inside a SmartLinks container, widget, or iframe.
|
|
5679
|
+
* Defaults to auto-detection via `window.parent !== window`.
|
|
5680
|
+
embedded?: boolean;
|
|
5681
|
+
* Override for the `postMessage` target window.
|
|
5682
|
+
* Defaults to `window.parent`. Useful in tests and hosts that proxy messages.
|
|
5683
|
+
postTarget?: Window | null;
|
|
5684
|
+
* Override for the navigation window.
|
|
5685
|
+
* Defaults to `window`. Useful in tests.
|
|
5686
|
+
win?: Window;
|
|
5687
|
+
* When provided, `resolveLink` automatically fires a `click_link` analytics
|
|
5688
|
+
* event via `SL.analytics.browser.trackLinkClick` immediately before
|
|
5689
|
+
* navigating. Supply at minimum `collectionId`; add `productId`, `proofId`,
|
|
5690
|
+
* or any other `CollectionAnalyticsEvent` fields you want on the event.
|
|
5691
|
+
*
|
|
5692
|
+
* The resolver derives `isExternal`, `destinationAppId`, `linkTitle`, and
|
|
5693
|
+
* `href` from the `LinkTarget` automatically. Fields you supply here take
|
|
5694
|
+
* precedence over the derived values if there is a conflict.
|
|
5695
|
+
*
|
|
5696
|
+
* Called synchronously so the event fires even for external `_blank` links
|
|
5697
|
+
* that unload the page immediately after.
|
|
5698
|
+
*
|
|
5699
|
+
* @example
|
|
5700
|
+
* SL.navigation.resolveLink(link, {
|
|
5701
|
+
* track: { collectionId, productId },
|
|
5702
|
+
* });
|
|
5703
|
+
track?: LinkTrackingContext;
|
|
5704
|
+
}
|
|
5705
|
+
```
|
|
5706
|
+
|
|
5707
|
+
**ResolvedLink** (interface)
|
|
5708
|
+
```typescript
|
|
5709
|
+
interface ResolvedLink {
|
|
5710
|
+
navigate(): void;
|
|
5711
|
+
describe(): string;
|
|
5712
|
+
}
|
|
5713
|
+
```
|
|
5714
|
+
|
|
5715
|
+
**LinkOpenTarget** = `'_self' | '_blank'`
|
|
5716
|
+
|
|
5717
|
+
**LinkTarget** = ``
|
|
5718
|
+
|
|
5719
|
+
**LinkTrackingContext** (type)
|
|
5720
|
+
```typescript
|
|
5721
|
+
type LinkTrackingContext = { collectionId: string }
|
|
5722
|
+
```
|
|
5723
|
+
|
|
5672
5724
|
### nfc
|
|
5673
5725
|
|
|
5674
5726
|
**NfcTagInfo** (interface)
|
|
@@ -8560,6 +8612,11 @@ List available AI models
|
|
|
8560
8612
|
**get**(collectionId: string, modelId: string) → `Promise<AIModel>`
|
|
8561
8613
|
Get specific model information
|
|
8562
8614
|
|
|
8615
|
+
### navigation
|
|
8616
|
+
|
|
8617
|
+
**resolveLink**(link: LinkTarget, ctx: ResolveLinkContext = {}) → `ResolvedLink`
|
|
8618
|
+
Resolve a stored `LinkTarget` into an executable navigation action. The resolver handles the embedded/standalone distinction automatically: | Kind | Embedded (container/widget/iframe) | Standalone | |-------------------|---------------------------------------------------------------|-------------------| | `external _blank` | `window.open(url, '_blank', 'noopener,noreferrer')` | same | | `external _self` | `window.location.assign(url)` | same | | `app` / `deep` | `postMessage` to parent shell; shell appends context params | hash-route assign | Context params (`collectionId`, `productId`, `proofId`, etc.) are **not** included in the message payload — the parent SmartLinks shell owns them and appends them before broadcasting the navigation event. const r = SL.navigation.resolveLink(config.ctaLink); r.navigate(); // perform navigation button.ariaLabel = r.describe(); // human-readable label
|
|
8619
|
+
|
|
8563
8620
|
### nfc
|
|
8564
8621
|
|
|
8565
8622
|
**claimTag**(data: NfcClaimTagRequest) → `Promise<NfcTagInfo>`
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Complete guide to registering and discovering navigable states in SmartLinks apps, enabling portal menus, AI orchestrators, and other apps to deep-link directly into specific views.
|
|
4
4
|
|
|
5
|
+
This doc covers both sides of the contract: **suppliers** declare their navigable states in the manifest and app config; **consumers** discover those states at runtime, present them in a `LinkPicker` (admin), and resolve them with `SL.navigation.resolveLink` (public widgets and executors). External URLs and in-platform deep links are both first-class `LinkTarget` variants — a single type persisted, a single resolver called at the point of navigation.
|
|
6
|
+
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## Table of Contents
|
|
@@ -19,7 +21,8 @@ Complete guide to registering and discovering navigable states in SmartLinks app
|
|
|
19
21
|
- [Portal Navigation Menu](#portal-navigation-menu)
|
|
20
22
|
- [AI Orchestration](#ai-orchestration)
|
|
21
23
|
- [Cross-App Navigation](#cross-app-navigation)
|
|
22
|
-
- [
|
|
24
|
+
- [Link Picker — Admin Configuration](#link-picker--admin-configuration)
|
|
25
|
+
- [Rendering Links at Runtime](#rendering-links-at-runtime)
|
|
23
26
|
- [TypeScript Types](#typescript-types)
|
|
24
27
|
- [Supplier Responsibilities](#supplier-responsibilities)
|
|
25
28
|
- [Consumer Responsibilities](#consumer-responsibilities)
|
|
@@ -95,10 +98,11 @@ Both roles are required for end-to-end cross-app linking to work. An app that on
|
|
|
95
98
|
### Consumer checklist
|
|
96
99
|
|
|
97
100
|
- [ ] Uses `SL.collection.getAppsConfig(collectionId)` to discover installed apps — never hard-codes `appId`s
|
|
98
|
-
- [ ] Merges `manifest.linkable` + `config.linkable` to present
|
|
99
|
-
- [ ] Persists
|
|
100
|
-
- [ ]
|
|
101
|
-
- [ ]
|
|
101
|
+
- [ ] Merges `manifest.linkable` + `config.linkable` to present navigable states in the picker
|
|
102
|
+
- [ ] Persists the admin's choice as a `LinkTarget` discriminated union — never a raw URL string
|
|
103
|
+
- [ ] Renders / navigates using `SL.navigation.resolveLink(link)` — never `window.open` or `<a href>` directly
|
|
104
|
+
- [ ] Uses `<LinkPicker />` from `@proveanything/smartlinks-utils-ui` in admin — never a free-text field for app IDs or URLs
|
|
105
|
+
- [ ] External URLs are configured through `LinkPicker` too (kind: `'external'`), not a separate field
|
|
102
106
|
|
|
103
107
|
---
|
|
104
108
|
|
|
@@ -496,25 +500,37 @@ if (entry) {
|
|
|
496
500
|
|
|
497
501
|
---
|
|
498
502
|
|
|
499
|
-
###
|
|
503
|
+
### Link Picker — Admin Configuration
|
|
500
504
|
|
|
501
|
-
When an admin needs to configure
|
|
505
|
+
When an admin needs to configure any outbound link — to another installed app, a specific page within an app, or an external URL — **never use a bare text input**. Use `<LinkPicker />` from `@proveanything/smartlinks-utils-ui`. It handles all three `LinkTarget` kinds and produces a single `LinkTarget` value to persist.
|
|
502
506
|
|
|
503
|
-
|
|
507
|
+
```tsx
|
|
508
|
+
import { LinkPicker, type LinkTarget } from '@proveanything/smartlinks-utils-ui';
|
|
509
|
+
|
|
510
|
+
<LinkPicker
|
|
511
|
+
collectionId={collectionId}
|
|
512
|
+
currentAppId={appId} // include self in the app list
|
|
513
|
+
value={config.ctaLink} // LinkTarget | null
|
|
514
|
+
onChange={(next) => setConfig({ ...config, ctaLink: next })}
|
|
515
|
+
label="Button destination"
|
|
516
|
+
helpText="Choose an installed app, a specific page, or an external URL."
|
|
517
|
+
/>
|
|
518
|
+
```
|
|
504
519
|
|
|
505
|
-
|
|
506
|
-
- The target app is re-deployed on a new domain.
|
|
507
|
-
- The user is inside a container and full-page navigation would destroy their session.
|
|
508
|
-
- Platform context (`collectionId`, `productId`, `proofId`) needs to flow to the destination — URLs lose it, deep links keep it.
|
|
520
|
+
`LinkPicker` internally calls `SL.collection.getAppsConfig`, merges `manifest.linkable` + `config.linkable` for whichever app is selected, and lets the admin pick the link kind (external / app / deep link). You never need to build this UI yourself.
|
|
509
521
|
|
|
510
|
-
####
|
|
522
|
+
#### What `LinkPicker` produces
|
|
511
523
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
524
|
+
| Admin selects | `LinkTarget` stored |
|
|
525
|
+
|---------------|---------------------|
|
|
526
|
+
| External URL (opens in new tab) | `{ kind: 'external', url: '…', target: '_blank', rel: 'noopener noreferrer' }` |
|
|
527
|
+
| External URL (same tab) | `{ kind: 'external', url: '…', target: '_self' }` |
|
|
528
|
+
| An installed app, no specific page | `{ kind: 'app', appId: '…' }` |
|
|
529
|
+
| An installed app + specific page | `{ kind: 'deep', appId: '…', deepLinkId: '…', params: {…} }` |
|
|
516
530
|
|
|
517
|
-
####
|
|
531
|
+
#### How app/deep links are discovered inside the picker
|
|
532
|
+
|
|
533
|
+
The picker calls `SL.collection.getAppsConfig` and for the chosen app merges:
|
|
518
534
|
|
|
519
535
|
```typescript
|
|
520
536
|
const entries: DeepLinkEntry[] = [
|
|
@@ -523,94 +539,99 @@ const entries: DeepLinkEntry[] = [
|
|
|
523
539
|
];
|
|
524
540
|
```
|
|
525
541
|
|
|
526
|
-
If an app has no entries,
|
|
542
|
+
If an app has no declared entries, the picker offers the `app` kind ("Open app — default route") so the admin can still configure a link to it.
|
|
543
|
+
|
|
544
|
+
> **`kind: 'app'` and discoverability:** An app linked via `kind: 'app'` opens to its default route. If you want that default landing to have a meaningful title in the picker and in AI orchestration, declare a `linkable` entry for `"/"` in your manifest — it will appear as an explicit option rather than the fallback.
|
|
545
|
+
|
|
546
|
+
#### Persisting the value
|
|
527
547
|
|
|
528
|
-
|
|
548
|
+
`LinkTarget` is the persistence type. Store it as-is in your app config — never convert to a URL string before saving.
|
|
529
549
|
|
|
530
550
|
```typescript
|
|
531
|
-
// Correct
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
551
|
+
// ✅ Correct
|
|
552
|
+
await SL.appConfiguration.setConfig({
|
|
553
|
+
collectionId, appId,
|
|
554
|
+
config: { ...config, ctaLink: linkTarget },
|
|
555
|
+
admin: true,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// ❌ Wrong — you've thrown away the kind, params, and context-injection
|
|
559
|
+
await SL.appConfiguration.setConfig({
|
|
560
|
+
collectionId, appId,
|
|
561
|
+
config: { ...config, ctaUrl: resolvedUrl },
|
|
562
|
+
admin: true,
|
|
563
|
+
});
|
|
538
564
|
```
|
|
539
565
|
|
|
540
|
-
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
### Rendering Links at Runtime
|
|
541
569
|
|
|
542
|
-
|
|
570
|
+
In public widgets, executors, and any non-admin context, resolve a stored `LinkTarget` with `SL.navigation.resolveLink`. This is pure SDK — no React, no admin package dependency.
|
|
543
571
|
|
|
544
572
|
```typescript
|
|
545
|
-
|
|
573
|
+
import * as SL from '@proveanything/smartlinks';
|
|
574
|
+
import type { LinkTarget } from '@proveanything/smartlinks';
|
|
546
575
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
});
|
|
576
|
+
const resolved = SL.navigation.resolveLink(link);
|
|
577
|
+
|
|
578
|
+
resolved.navigate(); // executes the navigation (postMessage, location.assign, or window.open)
|
|
579
|
+
resolved.describe(); // plain-text label for aria-label, logging, AI agent descriptions
|
|
552
580
|
```
|
|
553
581
|
|
|
554
|
-
|
|
582
|
+
`resolveLink` handles the embedded/standalone distinction automatically:
|
|
555
583
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
584
|
+
| `LinkTarget.kind` | Inside container / iframe | Standalone (direct URL) |
|
|
585
|
+
|-------------------|--------------------------|-------------------------|
|
|
586
|
+
| `external` + `_blank` | `window.open(url, '_blank', 'noopener,noreferrer')` | same |
|
|
587
|
+
| `external` + `_self` | `window.location.assign(url)` | same |
|
|
588
|
+
| `app` / `deep` | `postMessage` to parent shell — shell appends `collectionId`, `productId`, `proofId`, etc. | constructs hash route locally and assigns/opens |
|
|
589
|
+
|
|
590
|
+
You never need to branch on `embedded` yourself — `resolveLink` reads the execution context.
|
|
560
591
|
|
|
561
|
-
|
|
562
|
-
SL.collection.getAppsConfig(collectionId, { admin: true }).then(setApps);
|
|
563
|
-
}, [collectionId]);
|
|
592
|
+
#### React component pattern
|
|
564
593
|
|
|
594
|
+
```tsx
|
|
595
|
+
import * as SL from '@proveanything/smartlinks';
|
|
596
|
+
import type { LinkTarget } from '@proveanything/smartlinks';
|
|
597
|
+
|
|
598
|
+
function CTAButton({ link, label }: { link: LinkTarget; label: string }) {
|
|
599
|
+
const resolved = SL.navigation.resolveLink(link);
|
|
565
600
|
return (
|
|
566
|
-
|
|
567
|
-
{
|
|
568
|
-
|
|
569
|
-
<option value="">— Choose an app —</option>
|
|
570
|
-
{apps.map(a => (
|
|
571
|
-
<option key={a.appId} value={a.appId}>{a.manifest?.meta?.name ?? a.appId}</option>
|
|
572
|
-
))}
|
|
573
|
-
</select>
|
|
574
|
-
|
|
575
|
-
{/* Page dropdown for chosen app */}
|
|
576
|
-
{value?.appId && (() => {
|
|
577
|
-
const app = apps.find(a => a.appId === value.appId);
|
|
578
|
-
const entries = [
|
|
579
|
-
...(app?.manifest?.linkable ?? []),
|
|
580
|
-
...(app?.config?.linkable ?? []),
|
|
581
|
-
];
|
|
582
|
-
if (entries.length === 0) {
|
|
583
|
-
return <select disabled><option>Default route only</option></select>;
|
|
584
|
-
}
|
|
585
|
-
return (
|
|
586
|
-
<select onChange={e => {
|
|
587
|
-
const entry = entries[+e.target.value];
|
|
588
|
-
onChange({ appId: value.appId, path: entry.path ?? '/', params: entry.params, label: entry.title });
|
|
589
|
-
}}>
|
|
590
|
-
{entries.map((entry, i) => (
|
|
591
|
-
<option key={i} value={i}>{entry.title}</option>
|
|
592
|
-
))}
|
|
593
|
-
</select>
|
|
594
|
-
);
|
|
595
|
-
})()}
|
|
596
|
-
</>
|
|
601
|
+
<button type="button" onClick={() => resolved.navigate()} aria-label={resolved.describe()}>
|
|
602
|
+
{label}
|
|
603
|
+
</button>
|
|
597
604
|
);
|
|
598
605
|
}
|
|
599
|
-
|
|
600
|
-
// --- Public side ---
|
|
601
|
-
const { onNavigate } = useAppContext();
|
|
602
|
-
{prizesLink && (
|
|
603
|
-
<button onClick={() => onNavigate({
|
|
604
|
-
appId: prizesLink.appId,
|
|
605
|
-
deepLink: prizesLink.path,
|
|
606
|
-
params: prizesLink.params,
|
|
607
|
-
})}>
|
|
608
|
-
See {prizesLink.label ?? 'the prizes'}
|
|
609
|
-
</button>
|
|
610
|
-
)}
|
|
611
606
|
```
|
|
612
607
|
|
|
613
|
-
|
|
608
|
+
#### Worked example — Raffle "See the prizes" button (full round-trip)
|
|
609
|
+
|
|
610
|
+
```tsx
|
|
611
|
+
// --- Admin app (uses LinkPicker from smartlinks-utils-ui) ---
|
|
612
|
+
import { LinkPicker, type LinkTarget } from '@proveanything/smartlinks-utils-ui';
|
|
613
|
+
|
|
614
|
+
<LinkPicker
|
|
615
|
+
collectionId={collectionId}
|
|
616
|
+
currentAppId={appId}
|
|
617
|
+
value={config.prizesLink}
|
|
618
|
+
onChange={(prizesLink) => setConfig({ ...config, prizesLink })}
|
|
619
|
+
label="Prizes page"
|
|
620
|
+
helpText="Link to a content app describing this raffle's prizes."
|
|
621
|
+
/>
|
|
622
|
+
|
|
623
|
+
// --- Public widget (uses SL.navigation.resolveLink from @proveanything/smartlinks) ---
|
|
624
|
+
import * as SL from '@proveanything/smartlinks';
|
|
625
|
+
|
|
626
|
+
function PrizesButton({ link }: { link: LinkTarget }) {
|
|
627
|
+
const r = SL.navigation.resolveLink(link);
|
|
628
|
+
return (
|
|
629
|
+
<button type="button" onClick={() => r.navigate()} aria-label={r.describe()}>
|
|
630
|
+
See the prizes
|
|
631
|
+
</button>
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
```
|
|
614
635
|
|
|
615
636
|
---
|
|
616
637
|
|
|
@@ -640,25 +661,32 @@ export interface DeepLinkEntry {
|
|
|
640
661
|
/** Convenience alias for an array of DeepLinkEntry */
|
|
641
662
|
export type DeepLinkRegistry = DeepLinkEntry[];
|
|
642
663
|
|
|
664
|
+
/** Where the link opens once resolved */
|
|
665
|
+
export type LinkOpenTarget = '_self' | '_blank';
|
|
666
|
+
|
|
643
667
|
/**
|
|
644
|
-
*
|
|
645
|
-
*
|
|
668
|
+
* Discriminated union representing any link a `LinkPicker` can produce.
|
|
669
|
+
* This is the persistence type — store it as-is in app config; never convert to a URL.
|
|
646
670
|
*
|
|
647
|
-
*
|
|
648
|
-
* renamed or removed pages.
|
|
671
|
+
* Resolved at render time with `SL.navigation.resolveLink(link)`.
|
|
649
672
|
*/
|
|
650
|
-
export
|
|
651
|
-
/**
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
673
|
+
export type LinkTarget =
|
|
674
|
+
/** A URL outside the SmartLinks platform */
|
|
675
|
+
| { kind: 'external'; url: string; target: LinkOpenTarget; rel?: string }
|
|
676
|
+
/** Open an installed app at its default route */
|
|
677
|
+
| { kind: 'app'; appId: string; target?: LinkOpenTarget }
|
|
678
|
+
/** Open a specific deep-linkable page within an installed app */
|
|
679
|
+
| { kind: 'deep'; appId: string; deepLinkId: string;
|
|
680
|
+
params?: Record<string, string>; target?: LinkOpenTarget };
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* The object returned by `SL.navigation.resolveLink`.
|
|
684
|
+
* `navigate()` performs the navigation; `describe()` returns a plain-text label
|
|
685
|
+
* suitable for aria-label attributes and AI agent descriptions.
|
|
686
|
+
*/
|
|
687
|
+
export interface ResolvedLink {
|
|
688
|
+
navigate(): void;
|
|
689
|
+
describe(): string;
|
|
662
690
|
}
|
|
663
691
|
```
|
|
664
692
|
|
|
@@ -681,16 +709,16 @@ Rules for apps that **expose** navigable states to the platform:
|
|
|
681
709
|
|
|
682
710
|
## Consumer Responsibilities
|
|
683
711
|
|
|
684
|
-
Rules for apps that **navigate to** states in other installed apps:
|
|
712
|
+
Rules for apps that **navigate to** states in other installed apps or external URLs:
|
|
685
713
|
|
|
686
714
|
1. **Never hard-code `appId`s or URLs** — always discover installed apps via `SL.collection.getAppsConfig(collectionId)`.
|
|
687
715
|
2. **Merge both sources** — always combine `manifest.linkable` and `config.linkable`; never assume all links come from either source alone.
|
|
688
|
-
3. **Persist as `
|
|
689
|
-
4. **Navigate with `
|
|
690
|
-
5. **
|
|
691
|
-
6. **
|
|
716
|
+
3. **Persist as `LinkTarget`** — store the discriminated union `{ kind, … }` as-is in your app config; never convert to a raw URL string before saving.
|
|
717
|
+
4. **Navigate with `SL.navigation.resolveLink`** — call `resolveLink(link).navigate()` for all outbound navigation; never call `window.open`, `window.location.assign`, or `<a href>` directly from app logic.
|
|
718
|
+
5. **Use `<LinkPicker />` in admin** — admin forms that configure any outbound link (internal or external) must use `LinkPicker` from `@proveanything/smartlinks-utils-ui`, never a free-text field.
|
|
719
|
+
6. **External URLs belong in `LinkPicker` too** — `kind: 'external'` is a first-class option, not a separate field. One picker, one stored shape, one resolver.
|
|
692
720
|
7. **Handle missing entries gracefully** — older apps won't have manifest `linkable` entries or `appConfig.linkable`. Always use `?? []` on both sources.
|
|
693
|
-
8. **Show
|
|
721
|
+
8. **Show fallback for apps with no declared entries** — `LinkPicker` uses `kind: 'app'` (default route) so admins can still link to an app that hasn't declared any deep-linkable pages.
|
|
694
722
|
|
|
695
723
|
---
|
|
696
724
|
|
|
@@ -698,13 +726,14 @@ Rules for apps that **navigate to** states in other installed apps:
|
|
|
698
726
|
|
|
699
727
|
| ❌ Anti-pattern | ✅ Correct approach |
|
|
700
728
|
|----------------|---------------------|
|
|
701
|
-
| Free-text "Target app ID" field in admin UI |
|
|
702
|
-
|
|
|
703
|
-
| Hard-coded `https://…` links between apps in the same collection | `
|
|
704
|
-
| `window.open()` / `<a target="_blank">`
|
|
729
|
+
| Free-text "Target app ID" field in admin UI | `<LinkPicker />` from `smartlinks-utils-ui` |
|
|
730
|
+
| Separate "URL" field next to internal link field | `LinkPicker` handles both; `kind: 'external'` is a first-class option |
|
|
731
|
+
| Hard-coded `https://…` links between apps in the same collection | `kind: 'deep'` / `kind: 'app'` resolved with `SL.navigation.resolveLink` |
|
|
732
|
+
| `window.open()` / `<a target="_blank">` called directly from app logic | `SL.navigation.resolveLink(link).navigate()` |
|
|
733
|
+
| Converting a `LinkTarget` to a URL string before saving | Persist the `LinkTarget` union; resolve at navigation time |
|
|
705
734
|
| Writing static routes to `appConfig.linkable` on install | Declare them in `app.manifest.json` once, at build time |
|
|
706
735
|
| Putting `collectionId` / `productId` in `DeepLinkEntry.params` | Platform injects context automatically — omit them |
|
|
707
736
|
| Patching individual entries in `appConfig.linkable` | Full-replace the array on every sync |
|
|
708
|
-
|
|
|
709
|
-
| Re-implementing picker logic per app |
|
|
737
|
+
| Importing `LinkPicker` in a public widget or executor | `LinkPicker` is admin-only; use `SL.navigation.resolveLink` in public code |
|
|
738
|
+
| Re-implementing picker logic per app | Use `<LinkPicker />` from `@proveanything/smartlinks-utils-ui` |
|
|
710
739
|
|
package/dist/openapi.yaml
CHANGED
|
@@ -21044,6 +21044,28 @@ components:
|
|
|
21044
21044
|
type: string
|
|
21045
21045
|
required:
|
|
21046
21046
|
- points
|
|
21047
|
+
ResolveLinkContext:
|
|
21048
|
+
type: object
|
|
21049
|
+
properties:
|
|
21050
|
+
embedded:
|
|
21051
|
+
type: boolean
|
|
21052
|
+
postTarget:
|
|
21053
|
+
$ref: "#/components/schemas/Window"
|
|
21054
|
+
win:
|
|
21055
|
+
$ref: "#/components/schemas/Window"
|
|
21056
|
+
track:
|
|
21057
|
+
$ref: "#/components/schemas/LinkTrackingContext"
|
|
21058
|
+
ResolvedLink:
|
|
21059
|
+
type: object
|
|
21060
|
+
properties: {}
|
|
21061
|
+
LinkOpenTarget:
|
|
21062
|
+
type: string
|
|
21063
|
+
enum:
|
|
21064
|
+
- _self
|
|
21065
|
+
- _blank
|
|
21066
|
+
LinkTrackingContext:
|
|
21067
|
+
type: object
|
|
21068
|
+
properties: {}
|
|
21047
21069
|
NfcTagInfo:
|
|
21048
21070
|
type: object
|
|
21049
21071
|
properties:
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { CollectionAnalyticsEvent } from './analytics';
|
|
2
|
+
/** Where the link opens once resolved. */
|
|
3
|
+
export type LinkOpenTarget = '_self' | '_blank';
|
|
4
|
+
/**
|
|
5
|
+
* Discriminated union representing any outbound link a `LinkPicker` can produce.
|
|
6
|
+
*
|
|
7
|
+
* This is the **persistence type** — store as-is in app config; never convert to a
|
|
8
|
+
* URL string before saving. Resolve at navigation time with `SL.navigation.resolveLink`.
|
|
9
|
+
*
|
|
10
|
+
* - `external` — a URL outside the SmartLinks platform (supports same-tab or new-tab)
|
|
11
|
+
* - `app` — open an installed app at its default route
|
|
12
|
+
* - `deep` — open a specific deep-linkable page within an installed app
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // External link
|
|
16
|
+
* { kind: 'external', url: 'https://example.com', target: '_blank' }
|
|
17
|
+
*
|
|
18
|
+
* // Open an app at its default route
|
|
19
|
+
* { kind: 'app', appId: 'raffle' }
|
|
20
|
+
*
|
|
21
|
+
* // Open a specific page within an app
|
|
22
|
+
* { kind: 'deep', appId: 'prizes', deepLinkId: '/winners', params: { year: '2026' } }
|
|
23
|
+
*/
|
|
24
|
+
export type LinkTarget = {
|
|
25
|
+
kind: 'external';
|
|
26
|
+
/** Absolute URL to navigate to. */
|
|
27
|
+
url: string;
|
|
28
|
+
target: LinkOpenTarget;
|
|
29
|
+
/**
|
|
30
|
+
* Window features string for `window.open`.
|
|
31
|
+
* Defaults to `'noopener,noreferrer'`; supply this only to override the
|
|
32
|
+
* default (e.g. to add `'popup'`). `noopener` and `noreferrer` are always
|
|
33
|
+
* included regardless of what is provided here.
|
|
34
|
+
*/
|
|
35
|
+
rel?: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'app';
|
|
38
|
+
/** The target app's `appId`. */
|
|
39
|
+
appId: string;
|
|
40
|
+
target?: LinkOpenTarget;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'deep';
|
|
43
|
+
/** The target app's `appId`. */
|
|
44
|
+
appId: string;
|
|
45
|
+
/**
|
|
46
|
+
* Identifies the deep-linkable page within the target app.
|
|
47
|
+
* Corresponds to `DeepLinkEntry.path` (or `path?params`) as declared in
|
|
48
|
+
* `app.manifest.json#linkable` or `appConfig.linkable`.
|
|
49
|
+
*/
|
|
50
|
+
deepLinkId: string;
|
|
51
|
+
/** App-specific query params. Platform context params are injected automatically. */
|
|
52
|
+
params?: Record<string, string>;
|
|
53
|
+
target?: LinkOpenTarget;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Platform context fields passed to `resolveLink` to enable automatic link-click
|
|
57
|
+
* tracking via `SL.analytics.browser.trackLinkClick`.
|
|
58
|
+
*
|
|
59
|
+
* `collectionId` is required; all other `CollectionAnalyticsEvent` fields are
|
|
60
|
+
* optional and will be merged into the fired event. The resolver derives
|
|
61
|
+
* `isExternal`, `destinationAppId`, `linkTitle`, and `href` automatically from
|
|
62
|
+
* the `LinkTarget` — supply any of those here only if you need to override them.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* SL.navigation.resolveLink(link, {
|
|
66
|
+
* track: { collectionId, productId },
|
|
67
|
+
* });
|
|
68
|
+
*/
|
|
69
|
+
export type LinkTrackingContext = {
|
|
70
|
+
collectionId: string;
|
|
71
|
+
} & Partial<Omit<CollectionAnalyticsEvent, 'collectionId' | 'eventType'>>;
|
|
72
|
+
/**
|
|
73
|
+
* Context passed to `SL.navigation.resolveLink` to control execution environment.
|
|
74
|
+
* All fields are optional — the resolver detects sensible defaults at runtime.
|
|
75
|
+
*/
|
|
76
|
+
export interface ResolveLinkContext {
|
|
77
|
+
/**
|
|
78
|
+
* True when running inside a SmartLinks container, widget, or iframe.
|
|
79
|
+
* Defaults to auto-detection via `window.parent !== window`.
|
|
80
|
+
*/
|
|
81
|
+
embedded?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Override for the `postMessage` target window.
|
|
84
|
+
* Defaults to `window.parent`. Useful in tests and hosts that proxy messages.
|
|
85
|
+
*/
|
|
86
|
+
postTarget?: Window | null;
|
|
87
|
+
/**
|
|
88
|
+
* Override for the navigation window.
|
|
89
|
+
* Defaults to `window`. Useful in tests.
|
|
90
|
+
*/
|
|
91
|
+
win?: Window;
|
|
92
|
+
/**
|
|
93
|
+
* When provided, `resolveLink` automatically fires a `click_link` analytics
|
|
94
|
+
* event via `SL.analytics.browser.trackLinkClick` immediately before
|
|
95
|
+
* navigating. Supply at minimum `collectionId`; add `productId`, `proofId`,
|
|
96
|
+
* or any other `CollectionAnalyticsEvent` fields you want on the event.
|
|
97
|
+
*
|
|
98
|
+
* The resolver derives `isExternal`, `destinationAppId`, `linkTitle`, and
|
|
99
|
+
* `href` from the `LinkTarget` automatically. Fields you supply here take
|
|
100
|
+
* precedence over the derived values if there is a conflict.
|
|
101
|
+
*
|
|
102
|
+
* Called synchronously so the event fires even for external `_blank` links
|
|
103
|
+
* that unload the page immediately after.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* SL.navigation.resolveLink(link, {
|
|
107
|
+
* track: { collectionId, productId },
|
|
108
|
+
* });
|
|
109
|
+
*/
|
|
110
|
+
track?: LinkTrackingContext;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The object returned by `SL.navigation.resolveLink`.
|
|
114
|
+
*
|
|
115
|
+
* - `navigate()` — performs the navigation (postMessage, location.assign, or window.open).
|
|
116
|
+
* - `describe()` — returns a plain-text summary suitable for `aria-label` and AI agents.
|
|
117
|
+
*/
|
|
118
|
+
export interface ResolvedLink {
|
|
119
|
+
navigate(): void;
|
|
120
|
+
describe(): string;
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.11.
|
|
3
|
+
Version: 1.11.13 | Generated: 2026-05-05T09:55:20.018Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -110,6 +110,7 @@ The Smartlinks SDK is organized into the following namespaces:
|
|
|
110
110
|
- **jobs** - Functions for jobs operations
|
|
111
111
|
- **journeysAnalytics** - Functions for journeysAnalytics operations
|
|
112
112
|
- **location** - Functions for location operations
|
|
113
|
+
- **navigation** - Functions for navigation operations
|
|
113
114
|
- **order** - Functions for order operations
|
|
114
115
|
- **products** - Functions for products operations
|
|
115
116
|
- **realtime** - Functions for realtime operations
|
|
@@ -5669,6 +5670,57 @@ interface RecordLoyaltyTransactionBody {
|
|
|
5669
5670
|
|
|
5670
5671
|
**DataBlock** = `Record<string, unknown>`
|
|
5671
5672
|
|
|
5673
|
+
### navigation
|
|
5674
|
+
|
|
5675
|
+
**ResolveLinkContext** (interface)
|
|
5676
|
+
```typescript
|
|
5677
|
+
interface ResolveLinkContext {
|
|
5678
|
+
* True when running inside a SmartLinks container, widget, or iframe.
|
|
5679
|
+
* Defaults to auto-detection via `window.parent !== window`.
|
|
5680
|
+
embedded?: boolean;
|
|
5681
|
+
* Override for the `postMessage` target window.
|
|
5682
|
+
* Defaults to `window.parent`. Useful in tests and hosts that proxy messages.
|
|
5683
|
+
postTarget?: Window | null;
|
|
5684
|
+
* Override for the navigation window.
|
|
5685
|
+
* Defaults to `window`. Useful in tests.
|
|
5686
|
+
win?: Window;
|
|
5687
|
+
* When provided, `resolveLink` automatically fires a `click_link` analytics
|
|
5688
|
+
* event via `SL.analytics.browser.trackLinkClick` immediately before
|
|
5689
|
+
* navigating. Supply at minimum `collectionId`; add `productId`, `proofId`,
|
|
5690
|
+
* or any other `CollectionAnalyticsEvent` fields you want on the event.
|
|
5691
|
+
*
|
|
5692
|
+
* The resolver derives `isExternal`, `destinationAppId`, `linkTitle`, and
|
|
5693
|
+
* `href` from the `LinkTarget` automatically. Fields you supply here take
|
|
5694
|
+
* precedence over the derived values if there is a conflict.
|
|
5695
|
+
*
|
|
5696
|
+
* Called synchronously so the event fires even for external `_blank` links
|
|
5697
|
+
* that unload the page immediately after.
|
|
5698
|
+
*
|
|
5699
|
+
* @example
|
|
5700
|
+
* SL.navigation.resolveLink(link, {
|
|
5701
|
+
* track: { collectionId, productId },
|
|
5702
|
+
* });
|
|
5703
|
+
track?: LinkTrackingContext;
|
|
5704
|
+
}
|
|
5705
|
+
```
|
|
5706
|
+
|
|
5707
|
+
**ResolvedLink** (interface)
|
|
5708
|
+
```typescript
|
|
5709
|
+
interface ResolvedLink {
|
|
5710
|
+
navigate(): void;
|
|
5711
|
+
describe(): string;
|
|
5712
|
+
}
|
|
5713
|
+
```
|
|
5714
|
+
|
|
5715
|
+
**LinkOpenTarget** = `'_self' | '_blank'`
|
|
5716
|
+
|
|
5717
|
+
**LinkTarget** = ``
|
|
5718
|
+
|
|
5719
|
+
**LinkTrackingContext** (type)
|
|
5720
|
+
```typescript
|
|
5721
|
+
type LinkTrackingContext = { collectionId: string }
|
|
5722
|
+
```
|
|
5723
|
+
|
|
5672
5724
|
### nfc
|
|
5673
5725
|
|
|
5674
5726
|
**NfcTagInfo** (interface)
|
|
@@ -8560,6 +8612,11 @@ List available AI models
|
|
|
8560
8612
|
**get**(collectionId: string, modelId: string) → `Promise<AIModel>`
|
|
8561
8613
|
Get specific model information
|
|
8562
8614
|
|
|
8615
|
+
### navigation
|
|
8616
|
+
|
|
8617
|
+
**resolveLink**(link: LinkTarget, ctx: ResolveLinkContext = {}) → `ResolvedLink`
|
|
8618
|
+
Resolve a stored `LinkTarget` into an executable navigation action. The resolver handles the embedded/standalone distinction automatically: | Kind | Embedded (container/widget/iframe) | Standalone | |-------------------|---------------------------------------------------------------|-------------------| | `external _blank` | `window.open(url, '_blank', 'noopener,noreferrer')` | same | | `external _self` | `window.location.assign(url)` | same | | `app` / `deep` | `postMessage` to parent shell; shell appends context params | hash-route assign | Context params (`collectionId`, `productId`, `proofId`, etc.) are **not** included in the message payload — the parent SmartLinks shell owns them and appends them before broadcasting the navigation event. const r = SL.navigation.resolveLink(config.ctaLink); r.navigate(); // perform navigation button.ariaLabel = r.describe(); // human-readable label
|
|
8619
|
+
|
|
8563
8620
|
### nfc
|
|
8564
8621
|
|
|
8565
8622
|
**claimTag**(data: NfcClaimTagRequest) → `Promise<NfcTagInfo>`
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Complete guide to registering and discovering navigable states in SmartLinks apps, enabling portal menus, AI orchestrators, and other apps to deep-link directly into specific views.
|
|
4
4
|
|
|
5
|
+
This doc covers both sides of the contract: **suppliers** declare their navigable states in the manifest and app config; **consumers** discover those states at runtime, present them in a `LinkPicker` (admin), and resolve them with `SL.navigation.resolveLink` (public widgets and executors). External URLs and in-platform deep links are both first-class `LinkTarget` variants — a single type persisted, a single resolver called at the point of navigation.
|
|
6
|
+
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## Table of Contents
|
|
@@ -19,7 +21,8 @@ Complete guide to registering and discovering navigable states in SmartLinks app
|
|
|
19
21
|
- [Portal Navigation Menu](#portal-navigation-menu)
|
|
20
22
|
- [AI Orchestration](#ai-orchestration)
|
|
21
23
|
- [Cross-App Navigation](#cross-app-navigation)
|
|
22
|
-
- [
|
|
24
|
+
- [Link Picker — Admin Configuration](#link-picker--admin-configuration)
|
|
25
|
+
- [Rendering Links at Runtime](#rendering-links-at-runtime)
|
|
23
26
|
- [TypeScript Types](#typescript-types)
|
|
24
27
|
- [Supplier Responsibilities](#supplier-responsibilities)
|
|
25
28
|
- [Consumer Responsibilities](#consumer-responsibilities)
|
|
@@ -95,10 +98,11 @@ Both roles are required for end-to-end cross-app linking to work. An app that on
|
|
|
95
98
|
### Consumer checklist
|
|
96
99
|
|
|
97
100
|
- [ ] Uses `SL.collection.getAppsConfig(collectionId)` to discover installed apps — never hard-codes `appId`s
|
|
98
|
-
- [ ] Merges `manifest.linkable` + `config.linkable` to present
|
|
99
|
-
- [ ] Persists
|
|
100
|
-
- [ ]
|
|
101
|
-
- [ ]
|
|
101
|
+
- [ ] Merges `manifest.linkable` + `config.linkable` to present navigable states in the picker
|
|
102
|
+
- [ ] Persists the admin's choice as a `LinkTarget` discriminated union — never a raw URL string
|
|
103
|
+
- [ ] Renders / navigates using `SL.navigation.resolveLink(link)` — never `window.open` or `<a href>` directly
|
|
104
|
+
- [ ] Uses `<LinkPicker />` from `@proveanything/smartlinks-utils-ui` in admin — never a free-text field for app IDs or URLs
|
|
105
|
+
- [ ] External URLs are configured through `LinkPicker` too (kind: `'external'`), not a separate field
|
|
102
106
|
|
|
103
107
|
---
|
|
104
108
|
|
|
@@ -496,25 +500,37 @@ if (entry) {
|
|
|
496
500
|
|
|
497
501
|
---
|
|
498
502
|
|
|
499
|
-
###
|
|
503
|
+
### Link Picker — Admin Configuration
|
|
500
504
|
|
|
501
|
-
When an admin needs to configure
|
|
505
|
+
When an admin needs to configure any outbound link — to another installed app, a specific page within an app, or an external URL — **never use a bare text input**. Use `<LinkPicker />` from `@proveanything/smartlinks-utils-ui`. It handles all three `LinkTarget` kinds and produces a single `LinkTarget` value to persist.
|
|
502
506
|
|
|
503
|
-
|
|
507
|
+
```tsx
|
|
508
|
+
import { LinkPicker, type LinkTarget } from '@proveanything/smartlinks-utils-ui';
|
|
509
|
+
|
|
510
|
+
<LinkPicker
|
|
511
|
+
collectionId={collectionId}
|
|
512
|
+
currentAppId={appId} // include self in the app list
|
|
513
|
+
value={config.ctaLink} // LinkTarget | null
|
|
514
|
+
onChange={(next) => setConfig({ ...config, ctaLink: next })}
|
|
515
|
+
label="Button destination"
|
|
516
|
+
helpText="Choose an installed app, a specific page, or an external URL."
|
|
517
|
+
/>
|
|
518
|
+
```
|
|
504
519
|
|
|
505
|
-
|
|
506
|
-
- The target app is re-deployed on a new domain.
|
|
507
|
-
- The user is inside a container and full-page navigation would destroy their session.
|
|
508
|
-
- Platform context (`collectionId`, `productId`, `proofId`) needs to flow to the destination — URLs lose it, deep links keep it.
|
|
520
|
+
`LinkPicker` internally calls `SL.collection.getAppsConfig`, merges `manifest.linkable` + `config.linkable` for whichever app is selected, and lets the admin pick the link kind (external / app / deep link). You never need to build this UI yourself.
|
|
509
521
|
|
|
510
|
-
####
|
|
522
|
+
#### What `LinkPicker` produces
|
|
511
523
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
524
|
+
| Admin selects | `LinkTarget` stored |
|
|
525
|
+
|---------------|---------------------|
|
|
526
|
+
| External URL (opens in new tab) | `{ kind: 'external', url: '…', target: '_blank', rel: 'noopener noreferrer' }` |
|
|
527
|
+
| External URL (same tab) | `{ kind: 'external', url: '…', target: '_self' }` |
|
|
528
|
+
| An installed app, no specific page | `{ kind: 'app', appId: '…' }` |
|
|
529
|
+
| An installed app + specific page | `{ kind: 'deep', appId: '…', deepLinkId: '…', params: {…} }` |
|
|
516
530
|
|
|
517
|
-
####
|
|
531
|
+
#### How app/deep links are discovered inside the picker
|
|
532
|
+
|
|
533
|
+
The picker calls `SL.collection.getAppsConfig` and for the chosen app merges:
|
|
518
534
|
|
|
519
535
|
```typescript
|
|
520
536
|
const entries: DeepLinkEntry[] = [
|
|
@@ -523,94 +539,99 @@ const entries: DeepLinkEntry[] = [
|
|
|
523
539
|
];
|
|
524
540
|
```
|
|
525
541
|
|
|
526
|
-
If an app has no entries,
|
|
542
|
+
If an app has no declared entries, the picker offers the `app` kind ("Open app — default route") so the admin can still configure a link to it.
|
|
543
|
+
|
|
544
|
+
> **`kind: 'app'` and discoverability:** An app linked via `kind: 'app'` opens to its default route. If you want that default landing to have a meaningful title in the picker and in AI orchestration, declare a `linkable` entry for `"/"` in your manifest — it will appear as an explicit option rather than the fallback.
|
|
545
|
+
|
|
546
|
+
#### Persisting the value
|
|
527
547
|
|
|
528
|
-
|
|
548
|
+
`LinkTarget` is the persistence type. Store it as-is in your app config — never convert to a URL string before saving.
|
|
529
549
|
|
|
530
550
|
```typescript
|
|
531
|
-
// Correct
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
551
|
+
// ✅ Correct
|
|
552
|
+
await SL.appConfiguration.setConfig({
|
|
553
|
+
collectionId, appId,
|
|
554
|
+
config: { ...config, ctaLink: linkTarget },
|
|
555
|
+
admin: true,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// ❌ Wrong — you've thrown away the kind, params, and context-injection
|
|
559
|
+
await SL.appConfiguration.setConfig({
|
|
560
|
+
collectionId, appId,
|
|
561
|
+
config: { ...config, ctaUrl: resolvedUrl },
|
|
562
|
+
admin: true,
|
|
563
|
+
});
|
|
538
564
|
```
|
|
539
565
|
|
|
540
|
-
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
### Rendering Links at Runtime
|
|
541
569
|
|
|
542
|
-
|
|
570
|
+
In public widgets, executors, and any non-admin context, resolve a stored `LinkTarget` with `SL.navigation.resolveLink`. This is pure SDK — no React, no admin package dependency.
|
|
543
571
|
|
|
544
572
|
```typescript
|
|
545
|
-
|
|
573
|
+
import * as SL from '@proveanything/smartlinks';
|
|
574
|
+
import type { LinkTarget } from '@proveanything/smartlinks';
|
|
546
575
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
});
|
|
576
|
+
const resolved = SL.navigation.resolveLink(link);
|
|
577
|
+
|
|
578
|
+
resolved.navigate(); // executes the navigation (postMessage, location.assign, or window.open)
|
|
579
|
+
resolved.describe(); // plain-text label for aria-label, logging, AI agent descriptions
|
|
552
580
|
```
|
|
553
581
|
|
|
554
|
-
|
|
582
|
+
`resolveLink` handles the embedded/standalone distinction automatically:
|
|
555
583
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
584
|
+
| `LinkTarget.kind` | Inside container / iframe | Standalone (direct URL) |
|
|
585
|
+
|-------------------|--------------------------|-------------------------|
|
|
586
|
+
| `external` + `_blank` | `window.open(url, '_blank', 'noopener,noreferrer')` | same |
|
|
587
|
+
| `external` + `_self` | `window.location.assign(url)` | same |
|
|
588
|
+
| `app` / `deep` | `postMessage` to parent shell — shell appends `collectionId`, `productId`, `proofId`, etc. | constructs hash route locally and assigns/opens |
|
|
589
|
+
|
|
590
|
+
You never need to branch on `embedded` yourself — `resolveLink` reads the execution context.
|
|
560
591
|
|
|
561
|
-
|
|
562
|
-
SL.collection.getAppsConfig(collectionId, { admin: true }).then(setApps);
|
|
563
|
-
}, [collectionId]);
|
|
592
|
+
#### React component pattern
|
|
564
593
|
|
|
594
|
+
```tsx
|
|
595
|
+
import * as SL from '@proveanything/smartlinks';
|
|
596
|
+
import type { LinkTarget } from '@proveanything/smartlinks';
|
|
597
|
+
|
|
598
|
+
function CTAButton({ link, label }: { link: LinkTarget; label: string }) {
|
|
599
|
+
const resolved = SL.navigation.resolveLink(link);
|
|
565
600
|
return (
|
|
566
|
-
|
|
567
|
-
{
|
|
568
|
-
|
|
569
|
-
<option value="">— Choose an app —</option>
|
|
570
|
-
{apps.map(a => (
|
|
571
|
-
<option key={a.appId} value={a.appId}>{a.manifest?.meta?.name ?? a.appId}</option>
|
|
572
|
-
))}
|
|
573
|
-
</select>
|
|
574
|
-
|
|
575
|
-
{/* Page dropdown for chosen app */}
|
|
576
|
-
{value?.appId && (() => {
|
|
577
|
-
const app = apps.find(a => a.appId === value.appId);
|
|
578
|
-
const entries = [
|
|
579
|
-
...(app?.manifest?.linkable ?? []),
|
|
580
|
-
...(app?.config?.linkable ?? []),
|
|
581
|
-
];
|
|
582
|
-
if (entries.length === 0) {
|
|
583
|
-
return <select disabled><option>Default route only</option></select>;
|
|
584
|
-
}
|
|
585
|
-
return (
|
|
586
|
-
<select onChange={e => {
|
|
587
|
-
const entry = entries[+e.target.value];
|
|
588
|
-
onChange({ appId: value.appId, path: entry.path ?? '/', params: entry.params, label: entry.title });
|
|
589
|
-
}}>
|
|
590
|
-
{entries.map((entry, i) => (
|
|
591
|
-
<option key={i} value={i}>{entry.title}</option>
|
|
592
|
-
))}
|
|
593
|
-
</select>
|
|
594
|
-
);
|
|
595
|
-
})()}
|
|
596
|
-
</>
|
|
601
|
+
<button type="button" onClick={() => resolved.navigate()} aria-label={resolved.describe()}>
|
|
602
|
+
{label}
|
|
603
|
+
</button>
|
|
597
604
|
);
|
|
598
605
|
}
|
|
599
|
-
|
|
600
|
-
// --- Public side ---
|
|
601
|
-
const { onNavigate } = useAppContext();
|
|
602
|
-
{prizesLink && (
|
|
603
|
-
<button onClick={() => onNavigate({
|
|
604
|
-
appId: prizesLink.appId,
|
|
605
|
-
deepLink: prizesLink.path,
|
|
606
|
-
params: prizesLink.params,
|
|
607
|
-
})}>
|
|
608
|
-
See {prizesLink.label ?? 'the prizes'}
|
|
609
|
-
</button>
|
|
610
|
-
)}
|
|
611
606
|
```
|
|
612
607
|
|
|
613
|
-
|
|
608
|
+
#### Worked example — Raffle "See the prizes" button (full round-trip)
|
|
609
|
+
|
|
610
|
+
```tsx
|
|
611
|
+
// --- Admin app (uses LinkPicker from smartlinks-utils-ui) ---
|
|
612
|
+
import { LinkPicker, type LinkTarget } from '@proveanything/smartlinks-utils-ui';
|
|
613
|
+
|
|
614
|
+
<LinkPicker
|
|
615
|
+
collectionId={collectionId}
|
|
616
|
+
currentAppId={appId}
|
|
617
|
+
value={config.prizesLink}
|
|
618
|
+
onChange={(prizesLink) => setConfig({ ...config, prizesLink })}
|
|
619
|
+
label="Prizes page"
|
|
620
|
+
helpText="Link to a content app describing this raffle's prizes."
|
|
621
|
+
/>
|
|
622
|
+
|
|
623
|
+
// --- Public widget (uses SL.navigation.resolveLink from @proveanything/smartlinks) ---
|
|
624
|
+
import * as SL from '@proveanything/smartlinks';
|
|
625
|
+
|
|
626
|
+
function PrizesButton({ link }: { link: LinkTarget }) {
|
|
627
|
+
const r = SL.navigation.resolveLink(link);
|
|
628
|
+
return (
|
|
629
|
+
<button type="button" onClick={() => r.navigate()} aria-label={r.describe()}>
|
|
630
|
+
See the prizes
|
|
631
|
+
</button>
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
```
|
|
614
635
|
|
|
615
636
|
---
|
|
616
637
|
|
|
@@ -640,25 +661,32 @@ export interface DeepLinkEntry {
|
|
|
640
661
|
/** Convenience alias for an array of DeepLinkEntry */
|
|
641
662
|
export type DeepLinkRegistry = DeepLinkEntry[];
|
|
642
663
|
|
|
664
|
+
/** Where the link opens once resolved */
|
|
665
|
+
export type LinkOpenTarget = '_self' | '_blank';
|
|
666
|
+
|
|
643
667
|
/**
|
|
644
|
-
*
|
|
645
|
-
*
|
|
668
|
+
* Discriminated union representing any link a `LinkPicker` can produce.
|
|
669
|
+
* This is the persistence type — store it as-is in app config; never convert to a URL.
|
|
646
670
|
*
|
|
647
|
-
*
|
|
648
|
-
* renamed or removed pages.
|
|
671
|
+
* Resolved at render time with `SL.navigation.resolveLink(link)`.
|
|
649
672
|
*/
|
|
650
|
-
export
|
|
651
|
-
/**
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
673
|
+
export type LinkTarget =
|
|
674
|
+
/** A URL outside the SmartLinks platform */
|
|
675
|
+
| { kind: 'external'; url: string; target: LinkOpenTarget; rel?: string }
|
|
676
|
+
/** Open an installed app at its default route */
|
|
677
|
+
| { kind: 'app'; appId: string; target?: LinkOpenTarget }
|
|
678
|
+
/** Open a specific deep-linkable page within an installed app */
|
|
679
|
+
| { kind: 'deep'; appId: string; deepLinkId: string;
|
|
680
|
+
params?: Record<string, string>; target?: LinkOpenTarget };
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* The object returned by `SL.navigation.resolveLink`.
|
|
684
|
+
* `navigate()` performs the navigation; `describe()` returns a plain-text label
|
|
685
|
+
* suitable for aria-label attributes and AI agent descriptions.
|
|
686
|
+
*/
|
|
687
|
+
export interface ResolvedLink {
|
|
688
|
+
navigate(): void;
|
|
689
|
+
describe(): string;
|
|
662
690
|
}
|
|
663
691
|
```
|
|
664
692
|
|
|
@@ -681,16 +709,16 @@ Rules for apps that **expose** navigable states to the platform:
|
|
|
681
709
|
|
|
682
710
|
## Consumer Responsibilities
|
|
683
711
|
|
|
684
|
-
Rules for apps that **navigate to** states in other installed apps:
|
|
712
|
+
Rules for apps that **navigate to** states in other installed apps or external URLs:
|
|
685
713
|
|
|
686
714
|
1. **Never hard-code `appId`s or URLs** — always discover installed apps via `SL.collection.getAppsConfig(collectionId)`.
|
|
687
715
|
2. **Merge both sources** — always combine `manifest.linkable` and `config.linkable`; never assume all links come from either source alone.
|
|
688
|
-
3. **Persist as `
|
|
689
|
-
4. **Navigate with `
|
|
690
|
-
5. **
|
|
691
|
-
6. **
|
|
716
|
+
3. **Persist as `LinkTarget`** — store the discriminated union `{ kind, … }` as-is in your app config; never convert to a raw URL string before saving.
|
|
717
|
+
4. **Navigate with `SL.navigation.resolveLink`** — call `resolveLink(link).navigate()` for all outbound navigation; never call `window.open`, `window.location.assign`, or `<a href>` directly from app logic.
|
|
718
|
+
5. **Use `<LinkPicker />` in admin** — admin forms that configure any outbound link (internal or external) must use `LinkPicker` from `@proveanything/smartlinks-utils-ui`, never a free-text field.
|
|
719
|
+
6. **External URLs belong in `LinkPicker` too** — `kind: 'external'` is a first-class option, not a separate field. One picker, one stored shape, one resolver.
|
|
692
720
|
7. **Handle missing entries gracefully** — older apps won't have manifest `linkable` entries or `appConfig.linkable`. Always use `?? []` on both sources.
|
|
693
|
-
8. **Show
|
|
721
|
+
8. **Show fallback for apps with no declared entries** — `LinkPicker` uses `kind: 'app'` (default route) so admins can still link to an app that hasn't declared any deep-linkable pages.
|
|
694
722
|
|
|
695
723
|
---
|
|
696
724
|
|
|
@@ -698,13 +726,14 @@ Rules for apps that **navigate to** states in other installed apps:
|
|
|
698
726
|
|
|
699
727
|
| ❌ Anti-pattern | ✅ Correct approach |
|
|
700
728
|
|----------------|---------------------|
|
|
701
|
-
| Free-text "Target app ID" field in admin UI |
|
|
702
|
-
|
|
|
703
|
-
| Hard-coded `https://…` links between apps in the same collection | `
|
|
704
|
-
| `window.open()` / `<a target="_blank">`
|
|
729
|
+
| Free-text "Target app ID" field in admin UI | `<LinkPicker />` from `smartlinks-utils-ui` |
|
|
730
|
+
| Separate "URL" field next to internal link field | `LinkPicker` handles both; `kind: 'external'` is a first-class option |
|
|
731
|
+
| Hard-coded `https://…` links between apps in the same collection | `kind: 'deep'` / `kind: 'app'` resolved with `SL.navigation.resolveLink` |
|
|
732
|
+
| `window.open()` / `<a target="_blank">` called directly from app logic | `SL.navigation.resolveLink(link).navigate()` |
|
|
733
|
+
| Converting a `LinkTarget` to a URL string before saving | Persist the `LinkTarget` union; resolve at navigation time |
|
|
705
734
|
| Writing static routes to `appConfig.linkable` on install | Declare them in `app.manifest.json` once, at build time |
|
|
706
735
|
| Putting `collectionId` / `productId` in `DeepLinkEntry.params` | Platform injects context automatically — omit them |
|
|
707
736
|
| Patching individual entries in `appConfig.linkable` | Full-replace the array on every sync |
|
|
708
|
-
|
|
|
709
|
-
| Re-implementing picker logic per app |
|
|
737
|
+
| Importing `LinkPicker` in a public widget or executor | `LinkPicker` is admin-only; use `SL.navigation.resolveLink` in public code |
|
|
738
|
+
| Re-implementing picker logic per app | Use `<LinkPicker />` from `@proveanything/smartlinks-utils-ui` |
|
|
710
739
|
|
package/openapi.yaml
CHANGED
|
@@ -21044,6 +21044,28 @@ components:
|
|
|
21044
21044
|
type: string
|
|
21045
21045
|
required:
|
|
21046
21046
|
- points
|
|
21047
|
+
ResolveLinkContext:
|
|
21048
|
+
type: object
|
|
21049
|
+
properties:
|
|
21050
|
+
embedded:
|
|
21051
|
+
type: boolean
|
|
21052
|
+
postTarget:
|
|
21053
|
+
$ref: "#/components/schemas/Window"
|
|
21054
|
+
win:
|
|
21055
|
+
$ref: "#/components/schemas/Window"
|
|
21056
|
+
track:
|
|
21057
|
+
$ref: "#/components/schemas/LinkTrackingContext"
|
|
21058
|
+
ResolvedLink:
|
|
21059
|
+
type: object
|
|
21060
|
+
properties: {}
|
|
21061
|
+
LinkOpenTarget:
|
|
21062
|
+
type: string
|
|
21063
|
+
enum:
|
|
21064
|
+
- _self
|
|
21065
|
+
- _blank
|
|
21066
|
+
LinkTrackingContext:
|
|
21067
|
+
type: object
|
|
21068
|
+
properties: {}
|
|
21047
21069
|
NfcTagInfo:
|
|
21048
21070
|
type: object
|
|
21049
21071
|
properties:
|