@proveanything/smartlinks 1.14.11 → 1.14.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/docs/API_SUMMARY.md +7 -2
- package/dist/docs/overview.md +3 -0
- package/dist/docs/portal-auth-broadcast.md +26 -8
- package/dist/docs/portal-back-button.md +128 -36
- package/dist/docs/portal-request-action.md +289 -0
- package/dist/docs/portal-request-login.md +236 -0
- package/docs/API_SUMMARY.md +7 -2
- package/docs/overview.md +3 -0
- package/docs/portal-auth-broadcast.md +26 -8
- package/docs/portal-back-button.md +128 -36
- package/docs/portal-request-action.md +289 -0
- package/docs/portal-request-login.md +236 -0
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.14.
|
|
3
|
+
Version: 1.14.13 | Generated: 2026-05-19T11:49:34.090Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -36,6 +36,11 @@ For detailed guides on specific features:
|
|
|
36
36
|
- **[Product Facets SDK](PRODUCT_FACETS_SDK.md)** - Admin and public product facet endpoints and TypeScript interfaces
|
|
37
37
|
- **[Attestations](attestations.md)** - Append-only fact log with cryptographic chain integrity, time-series analytics, and public/owner/admin visibility
|
|
38
38
|
- **[Auth Kit](auth-kit.md)** - End-user authentication flows (email/password, magic link, OTP, OAuth) for microapps
|
|
39
|
+
- **[Portal Request Login](portal-request-login.md)** - Sub-apps delegate user authentication to the portal; `useSafeAuth().requestLogin` hook and iframe postMessage contract, redirect-bounce handling
|
|
40
|
+
- **[Portal Auth Broadcast](portal-auth-broadcast.md)** - Publishing a custom-flow session to the portal so the header, sibling apps, and SDK all stay in sync
|
|
41
|
+
- **[Portal Request Action](portal-request-action.md)** - Invoking portal built-in actions (`__qrScanner`, `__share`, `__logout`, etc.) from containers, widgets, and iframes
|
|
42
|
+
- **[Portal Back Button](portal-back-button.md)** - `parentPath` contract for hierarchy-aware "up" navigation; `useDeepLinkSync` integration
|
|
43
|
+
- **[Contact Search](contact-search.md)** - Admin contact search: free-text, typeahead, identity/tag/JSONB filters, and pagination
|
|
39
44
|
- **[App Data Storage](app-data-storage.md)** - User-specific and collection-scoped app data storage
|
|
40
45
|
- **[Forms](forms.md)** - Platform-managed form definitions, submissions, and schema-driven React form UI
|
|
41
46
|
- **[App Objects: Cases, Threads & Records](app-objects.md)** - Generic app-scoped building blocks for support cases, discussions, bookings, registrations, and more
|
|
@@ -89,7 +94,7 @@ The Smartlinks SDK is organized into the following namespaces:
|
|
|
89
94
|
— Identity & Access —
|
|
90
95
|
- **auth** - Admin authentication and account ops: login/logout, tokens, account info.
|
|
91
96
|
- **authKit** - End‑user auth flows (email/password, OAuth, phone); profiles and verification.
|
|
92
|
-
- **contact** - Manage customer contacts; CRUD, lookup, upsert, erase.
|
|
97
|
+
- **contact** - Manage customer contacts; CRUD, lookup, upsert, erase, and admin search. → [Guide](contact-search.md)
|
|
93
98
|
|
|
94
99
|
— Messaging & Audience —
|
|
95
100
|
- **comms** - Send notifications (push, email, wallet); templating, severity, delivery status. → [Guide](comms.md)
|
package/dist/docs/overview.md
CHANGED
|
@@ -65,6 +65,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
65
65
|
| **Executors** | `docs/executor.md` | Building executor bundles for SEO, LLM content, programmatic config |
|
|
66
66
|
| **Deep Linking** | `docs/deep-link-discovery.md` | URL state management, navigable states, portal menus, AI nav |
|
|
67
67
|
| **Portal Back Button** | `docs/portal-back-button.md` | Hierarchy-aware "up" navigation inside embedded apps |
|
|
68
|
+
| **Portal Request Action** | `docs/portal-request-action.md` | Triggering portal built-in actions (__qrScanner, __share, __logout, etc.) from sub-apps |
|
|
68
69
|
| **Interactions** | `docs/interactions.md` | Business events, outcomes, voting, competitions, and journey triggers |
|
|
69
70
|
| **AI-Native Manifests** | `docs/manifests.md` | `app.manifest.json`, `app.admin.json`, `ai-guide.md` structure |
|
|
70
71
|
| **App Config Files** | `docs/app-manifest.md` | Full field-by-field reference for both JSON config files |
|
|
@@ -74,7 +75,9 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
|
|
|
74
75
|
| **AI Guide Template** | `docs/ai-guide-template.md` | Template for creating `public/ai-guide.md` — customise per app |
|
|
75
76
|
| **Forms** | `docs/forms.md` | Form definitions, schema-driven rendering, submission patterns |
|
|
76
77
|
| **Auth Kit** | `docs/auth-kit.md` | End-user sign-in: email/password, magic links, phone OTP, Google OAuth |
|
|
78
|
+
| **Portal Request Login** | `docs/portal-request-login.md` | How sub-apps ask the portal to authenticate the user; hook and iframe postMessage contracts |
|
|
77
79
|
| **Portal Auth Broadcast** | `docs/portal-auth-broadcast.md` | Publishing custom auth flows to the portal; syncing sessions across containers and iframes |
|
|
80
|
+
| **Contact Search** | `docs/contact-search.md` | Admin contact search: free-text, typeahead, identity/tag/JSONB filters, and pagination |
|
|
78
81
|
| **App Records Pattern** | `docs/app-records-pattern.md` | Standard pattern for per-product/facet/variant/batch admin + public widget UIs |
|
|
79
82
|
| **UI Utils** | `docs/ui-utils.md` | `@proveanything/smartlinks-utils-ui` — React shells, hooks, and primitives for records-based apps |
|
|
80
83
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Publishing Auth State to the Portal
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **⚠️ Read [`portal-request-login.md`](./portal-request-login.md) first.**
|
|
4
|
+
> The recommended pattern for "I need a logged-in user before I continue"
|
|
5
|
+
> is `requestLogin` — the sub-app asks the portal to run its standard
|
|
6
|
+
> AuthKit flow and awaits a result. This doc covers the *other* case:
|
|
7
|
+
> a sub-app that runs its **own** authentication flow and needs to
|
|
8
|
+
> publish the resulting session up to the portal so the header, account
|
|
9
|
+
> UI, and sibling apps stay in sync.
|
|
4
10
|
|
|
5
11
|
A SmartLinks micro-app may need to run its own custom authentication flow —
|
|
6
12
|
typical cases: an auction app that calls a bidder API, a competition app
|
|
@@ -11,6 +17,7 @@ app pick up the new session.
|
|
|
11
17
|
|
|
12
18
|
This doc describes the contract the portal framework already implements.
|
|
13
19
|
|
|
20
|
+
|
|
14
21
|
---
|
|
15
22
|
|
|
16
23
|
## Container / Widget Apps (Same React Tree)
|
|
@@ -52,18 +59,28 @@ function BidButton() {
|
|
|
52
59
|
|
|
53
60
|
## Iframe Apps (Cross-Origin)
|
|
54
61
|
|
|
55
|
-
Iframe apps don't share React context
|
|
56
|
-
|
|
62
|
+
Iframe apps don't share React context with the portal. They publish their
|
|
63
|
+
session by posting framework-recognised messages on `window.parent`. The
|
|
64
|
+
portal's `IframeResponder` listens for these and routes them into the same
|
|
65
|
+
`login` / `logout` calls the built-in `AuthModal` makes.
|
|
66
|
+
|
|
67
|
+
> **Note:** There is no `authKit.publishLogin` / `publishLogout` helper in
|
|
68
|
+
> the SmartLinks SDK today. Use the raw `postMessage` calls below. If a
|
|
69
|
+
> helper ships later it will wrap exactly these payloads.
|
|
57
70
|
|
|
58
71
|
```ts
|
|
59
|
-
// LOGIN
|
|
72
|
+
// LOGIN — after your custom auth flow returns a token + user
|
|
60
73
|
window.parent.postMessage({
|
|
61
74
|
_smartlinksIframeMessage: true,
|
|
62
75
|
type: 'smartlinks:authkit:login',
|
|
63
76
|
payload: {
|
|
64
77
|
token: '<bearer>',
|
|
65
|
-
user: {
|
|
66
|
-
|
|
78
|
+
user: {
|
|
79
|
+
uid: 'usr_123',
|
|
80
|
+
email: 'bidder@example.com',
|
|
81
|
+
displayName: 'Jane Bidder',
|
|
82
|
+
},
|
|
83
|
+
accountData: { tier: 'gold' }, // optional, free-form
|
|
67
84
|
},
|
|
68
85
|
}, '*');
|
|
69
86
|
|
|
@@ -114,8 +131,9 @@ through the standard portal UI.
|
|
|
114
131
|
wiped.
|
|
115
132
|
|
|
116
133
|
❌ Implementing logout by just clearing your own state. Always call
|
|
117
|
-
`useAuth().logout()` (container/widget) or post
|
|
118
|
-
(iframe) so the whole portal
|
|
134
|
+
`useAuth().logout()` (container/widget) or post the
|
|
135
|
+
`smartlinks:authkit:logout` message (iframe) so the whole portal
|
|
136
|
+
session ends cleanly.
|
|
119
137
|
|
|
120
138
|
---
|
|
121
139
|
|
|
@@ -1,20 +1,36 @@
|
|
|
1
|
-
# Portal Back Button
|
|
1
|
+
# Portal Back Button — "Up" Navigation Inside Sub-Apps
|
|
2
2
|
|
|
3
|
-
> **For sub-app authors.**
|
|
3
|
+
> **For sub-app authors.** Drop this file into the SmartLinks SDK docs (e.g.
|
|
4
|
+
> `docs/portal-back-button.md`) and link it from `routing.md` / `mpa.md` so
|
|
5
|
+
> microapp authors discover it.
|
|
4
6
|
|
|
5
|
-
When a
|
|
7
|
+
When a sub-app is embedded by a portal shell, the shell renders a top-level
|
|
8
|
+
back button. By default that button **exits the app entirely** when tapped —
|
|
9
|
+
which is wrong for any app with internal hierarchy (lists → detail, wizards,
|
|
10
|
+
etc.).
|
|
6
11
|
|
|
7
|
-
This document describes the contract for telling the portal where "up" goes
|
|
12
|
+
This document describes the contract for telling the portal where "up" goes
|
|
13
|
+
from the current screen, so the top back button performs **hierarchy-aware
|
|
14
|
+
"up" navigation** inside your app instead.
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
---
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
## Why "up", not "back"
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
Browser back replays history. If a user navigates **list → item A → list →
|
|
21
|
+
item B**, browser back yo-yos: `B → list → A → list → ...`. That is not what
|
|
22
|
+
users expect from an "up arrow" in app chrome.
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
"Up" navigates the **content hierarchy**: `B → list → exit app`. It is
|
|
25
|
+
single-direction and idempotent. Only your app's router knows the hierarchy,
|
|
26
|
+
so the portal can't infer it — your app must declare it.
|
|
16
27
|
|
|
17
|
-
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## The Contract
|
|
31
|
+
|
|
32
|
+
With every `smartlinks-route-change` your app already posts (via
|
|
33
|
+
`useDeepLinkSync` or directly), include a `parentPath` field inside `state`:
|
|
18
34
|
|
|
19
35
|
```ts
|
|
20
36
|
window.parent.postMessage({
|
|
@@ -22,7 +38,8 @@ window.parent.postMessage({
|
|
|
22
38
|
path: '/items/123',
|
|
23
39
|
context: { collectionId: 'auction-2026', appId: 'auction' },
|
|
24
40
|
state: {
|
|
25
|
-
parentPath: '/items',
|
|
41
|
+
parentPath: '/items', // ← where "up" goes from /items/123
|
|
42
|
+
// ... your other app state
|
|
26
43
|
},
|
|
27
44
|
appId: 'auction',
|
|
28
45
|
}, '*');
|
|
@@ -30,43 +47,68 @@ window.parent.postMessage({
|
|
|
30
47
|
|
|
31
48
|
| `state.parentPath` value | Portal back-button behaviour |
|
|
32
49
|
| --- | --- |
|
|
33
|
-
| `'/items'` (or any string) |
|
|
34
|
-
| `''
|
|
50
|
+
| `'/items'` (or any string) | Posts `smartlinks-navigate` to the iframe with that path. App stays mounted. |
|
|
51
|
+
| `''` / missing / omitted | Default: clears the active app and returns to the portal homepage. |
|
|
52
|
+
|
|
53
|
+
When the portal sends the up-navigation message, your app must respond by
|
|
54
|
+
navigating to that path. If you use `useDeepLinkSync` with an SDK release
|
|
55
|
+
that includes the inbound `smartlinks-navigate` listener, this is automatic —
|
|
56
|
+
otherwise add a listener (see "Receiving the up-message" below).
|
|
35
57
|
|
|
36
|
-
|
|
58
|
+
---
|
|
37
59
|
|
|
38
|
-
## Recommended pattern
|
|
60
|
+
## Recommended sub-app pattern (React Router)
|
|
39
61
|
|
|
40
|
-
Keep
|
|
62
|
+
Keep "what's above this route" co-located with your route definitions:
|
|
41
63
|
|
|
42
64
|
```ts
|
|
65
|
+
// routes.ts
|
|
43
66
|
const PARENTS: Record<string, (params: any) => string | null> = {
|
|
44
|
-
'/':
|
|
45
|
-
'/items':
|
|
46
|
-
'/items/:id':
|
|
47
|
-
'/items/:id/bid':
|
|
48
|
-
'/checkout':
|
|
67
|
+
'/': () => null, // home — back exits the app
|
|
68
|
+
'/items': () => null, // list — back exits the app
|
|
69
|
+
'/items/:id': () => '/items', // detail — back goes to list
|
|
70
|
+
'/items/:id/bid': ({ id }) => `/items/${id}`,
|
|
71
|
+
'/checkout': () => '/items',
|
|
49
72
|
};
|
|
50
73
|
|
|
51
74
|
export function getParentPath(pathname: string): string | null {
|
|
52
|
-
//
|
|
53
|
-
|
|
75
|
+
// matchPath against PARENTS keys — return parent or null
|
|
76
|
+
// (use react-router's matchPath in your real implementation)
|
|
54
77
|
}
|
|
55
78
|
```
|
|
56
79
|
|
|
57
|
-
|
|
80
|
+
```tsx
|
|
81
|
+
// App.tsx
|
|
82
|
+
import { useDeepLinkSync } from '@/hooks/useDeepLinkSync';
|
|
83
|
+
import { getParentPath } from './routes';
|
|
84
|
+
|
|
85
|
+
function App() {
|
|
86
|
+
useDeepLinkSync({
|
|
87
|
+
getParentPath: ({ pathname }) => getParentPath(pathname),
|
|
88
|
+
});
|
|
89
|
+
// ...
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If your app does not use `useDeepLinkSync`, post the message yourself:
|
|
58
94
|
|
|
59
95
|
```ts
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (window.parent === window) return;
|
|
98
|
+
window.parent.postMessage({
|
|
99
|
+
type: 'smartlinks-route-change',
|
|
100
|
+
path: location.pathname,
|
|
101
|
+
context: { /* ... */ },
|
|
102
|
+
state: { parentPath: getParentPath(location.pathname) ?? '' },
|
|
103
|
+
}, '*');
|
|
104
|
+
}, [location.pathname]);
|
|
63
105
|
```
|
|
64
106
|
|
|
65
|
-
|
|
107
|
+
---
|
|
66
108
|
|
|
67
|
-
## Receiving the up-message
|
|
109
|
+
## Receiving the up-message (`smartlinks-navigate`)
|
|
68
110
|
|
|
69
|
-
When the portal consumes `parentPath`, it posts
|
|
111
|
+
When the portal back button consumes a `parentPath`, it posts:
|
|
70
112
|
|
|
71
113
|
```ts
|
|
72
114
|
{
|
|
@@ -78,18 +120,68 @@ When the portal consumes `parentPath`, it posts `smartlinks-navigate` back to th
|
|
|
78
120
|
}
|
|
79
121
|
```
|
|
80
122
|
|
|
81
|
-
Your app
|
|
123
|
+
Your app must listen for this and route accordingly. A minimal hook:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { useEffect } from 'react';
|
|
127
|
+
import { useNavigate } from 'react-router-dom';
|
|
128
|
+
|
|
129
|
+
export function useParentNavigation() {
|
|
130
|
+
const navigate = useNavigate();
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const onMessage = (e: MessageEvent) => {
|
|
133
|
+
if (e.data?.type !== 'smartlinks-navigate') return;
|
|
134
|
+
if (typeof e.data.path !== 'string') return;
|
|
135
|
+
navigate(e.data.path);
|
|
136
|
+
};
|
|
137
|
+
window.addEventListener('message', onMessage);
|
|
138
|
+
return () => window.removeEventListener('message', onMessage);
|
|
139
|
+
}, [navigate]);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Reference: Updated `RouteChangeMessage`
|
|
146
|
+
|
|
147
|
+
`state` is an open `Record<string, string>` — `parentPath` is a recognised
|
|
148
|
+
optional convention; everything else flows through unchanged.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
interface RouteChangeMessage {
|
|
152
|
+
type: 'smartlinks-route-change';
|
|
153
|
+
path: string;
|
|
154
|
+
context: Record<string, string>;
|
|
155
|
+
state: Record<string, string> & {
|
|
156
|
+
/**
|
|
157
|
+
* Optional. The "up" target from the current screen. When present, the
|
|
158
|
+
* portal's top back button posts smartlinks-navigate with this path
|
|
159
|
+
* instead of exiting the app. Empty / missing → exits the app.
|
|
160
|
+
*/
|
|
161
|
+
parentPath?: string;
|
|
162
|
+
};
|
|
163
|
+
appId?: string;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
82
166
|
|
|
83
|
-
|
|
167
|
+
No breaking change: existing apps that don't emit `parentPath` keep today's
|
|
168
|
+
behaviour (one tap of the portal back exits the app).
|
|
84
169
|
|
|
85
|
-
|
|
170
|
+
---
|
|
86
171
|
|
|
87
172
|
## FAQ
|
|
88
173
|
|
|
89
|
-
**Do I need to push every query-param change as a navigation step?**
|
|
174
|
+
**Q: Do I need to push every query-param change as a navigation step?**
|
|
175
|
+
No. `parentPath` is hierarchy, not history. A filter toggle, a tab switch,
|
|
176
|
+
or a modal open on the same screen should keep the same `parentPath`.
|
|
90
177
|
|
|
91
|
-
**What if my up
|
|
178
|
+
**Q: What if my "up" leaves the app?**
|
|
179
|
+
Set `parentPath` to `''` (or omit it). The portal will exit the app on back.
|
|
92
180
|
|
|
93
|
-
**Can I disable the portal back button on certain screens?**
|
|
181
|
+
**Q: Can I disable the portal back button on certain screens?**
|
|
182
|
+
Not directly via this protocol. Use the existing portal `homeScope: 'entry'`
|
|
183
|
+
mechanism if you need the entire portal back button suppressed.
|
|
94
184
|
|
|
95
|
-
**What about
|
|
185
|
+
**Q: What about the in-app back button I already render?**
|
|
186
|
+
Keep it if you want — they're not mutually exclusive. The portal back is
|
|
187
|
+
chrome; an in-content back is content. Many apps will only need one.
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Requesting Built-in Portal Actions
|
|
2
|
+
|
|
3
|
+
`requestAction` is the general-purpose sibling of
|
|
4
|
+
[`requestLogin`](./portal-request-login.md). It lets a sub-app
|
|
5
|
+
(container, widget, or iframe) trigger any of the portal's built-in
|
|
6
|
+
capabilities — open the QR scanner, log out, copy the current link,
|
|
7
|
+
navigate to another app — and await a result.
|
|
8
|
+
|
|
9
|
+
Use it whenever the answer to *"what do I want?"* is one of the
|
|
10
|
+
portal's standardized capabilities. The portal owns the implementation;
|
|
11
|
+
your sub-app declares intent.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why this exists
|
|
16
|
+
|
|
17
|
+
Linkpage-style apps, action grids and CTAs across the platform all need
|
|
18
|
+
the same primitives: scan a QR, take a photo, copy a link, jump to
|
|
19
|
+
another app, log the user out. Without a shared contract every app
|
|
20
|
+
re-implemented these against direct browser APIs, fragmenting UX and
|
|
21
|
+
breaking cross-iframe boundaries. `requestAction` is the contract.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Container / Widget Apps (Same React Tree)
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { usePortalActions } from '@proveanything/portal-framework';
|
|
29
|
+
|
|
30
|
+
function ShareButton() {
|
|
31
|
+
const portal = usePortalActions();
|
|
32
|
+
async function onClick() {
|
|
33
|
+
const result = await portal?.requestAction('__share', {
|
|
34
|
+
title: 'Check this out',
|
|
35
|
+
url: window.location.href,
|
|
36
|
+
});
|
|
37
|
+
// result.status === 'success' | 'cancelled' | 'unavailable' | 'error' | 'triggered'
|
|
38
|
+
}
|
|
39
|
+
return <button onClick={onClick}>Share</button>;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`usePortalActions()` returns `null` when no `ActionRequestProvider` is
|
|
44
|
+
mounted upstream — treat that the same as `status: 'unavailable'`.
|
|
45
|
+
|
|
46
|
+
### Discovering available actions
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const ids = portal?.getAvailableActions() ?? [];
|
|
50
|
+
// → ['__refresh','__share','__copyLink','__navigate','__login','__logout',
|
|
51
|
+
// '__account','__qrScanner','__barcodeScanner','__nfcScanner', ...]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Combine with `BUILTIN_ACTION_META` (also exported) for labels/icons in
|
|
55
|
+
your action pickers.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Iframe Apps (Cross-Origin)
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const requestId = crypto.randomUUID();
|
|
63
|
+
|
|
64
|
+
window.addEventListener('message', function handler(ev) {
|
|
65
|
+
const d = ev.data;
|
|
66
|
+
if (!d || d._smartlinksIframeMessage !== true) return;
|
|
67
|
+
if (d.type !== 'smartlinks:action:result') return;
|
|
68
|
+
if (d.payload?.requestId !== requestId) return;
|
|
69
|
+
window.removeEventListener('message', handler);
|
|
70
|
+
|
|
71
|
+
const { status, value, error } = d.payload;
|
|
72
|
+
// …handle result
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
window.parent.postMessage({
|
|
76
|
+
_smartlinksIframeMessage: true,
|
|
77
|
+
type: 'smartlinks:action:request',
|
|
78
|
+
payload: {
|
|
79
|
+
requestId,
|
|
80
|
+
actionId: '__copyLink',
|
|
81
|
+
params: { url: window.location.href },
|
|
82
|
+
},
|
|
83
|
+
}, '*');
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Capabilities probe
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
window.parent.postMessage({
|
|
90
|
+
_smartlinksIframeMessage: true,
|
|
91
|
+
type: 'smartlinks:action:capabilities',
|
|
92
|
+
payload: { requestId },
|
|
93
|
+
}, '*');
|
|
94
|
+
// reply: type 'smartlinks:action:capabilities:result'
|
|
95
|
+
// payload: { requestId, actions: string[], meta: Record<id, {label, icon, description, available}> }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Use this to hide actions the host doesn't support (e.g. `__nfcScanner`
|
|
99
|
+
on iOS web).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Built-in actions
|
|
104
|
+
|
|
105
|
+
| Action ID | Kind | Params | Success value |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| `__refresh` | sync | — | — |
|
|
108
|
+
| `__share` | sync | `{ title?, text?, url? }` | `{ method: 'native' \| 'clipboard' }` |
|
|
109
|
+
| `__copyLink` | sync | `{ text?, url? }` | `{ url }` |
|
|
110
|
+
| `__navigate` | sync | `{ appId, deepLink?, deepLinkParams? }` | — |
|
|
111
|
+
| `__login` | sync | `{ reason?, mode?, resolveIfAuthenticated? }` | `{ user, token }` |
|
|
112
|
+
| `__logout` | sync | — | — |
|
|
113
|
+
| `__account` | sync | login params | login → `{ user, token }`; logout → — |
|
|
114
|
+
| `__qrScanner` | overlay | `{ suppressNavigation?: boolean }` | `{ text, format, resolved?: { collectionId, productId?, proofId? } }` |
|
|
115
|
+
| `__barcodeScanner` | overlay | `{ suppressNavigation?: boolean }` | `{ text, format }` |
|
|
116
|
+
| `__nfcScanner` | overlay | `{ suppressNavigation?: boolean }` | `{ serial, payload? }` (Android only) |
|
|
117
|
+
| `__language` | overlay | — | — |
|
|
118
|
+
| `__profile` | page | — | — |
|
|
119
|
+
| `__settings` | page | — | — |
|
|
120
|
+
|
|
121
|
+
> **`__login` vs `requestLogin`:** For the common case of "I just need a
|
|
122
|
+
> logged-in user before continuing", prefer
|
|
123
|
+
> [`requestLogin`](./portal-request-login.md) — it is a typed convenience
|
|
124
|
+
> wrapper around `requestAction('__login', ...)` with a richer result
|
|
125
|
+
> contract. Use `__login` / `__account` via `requestAction` only when you
|
|
126
|
+
> are composing with other built-in actions in the same flow.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## App-registered actions
|
|
131
|
+
|
|
132
|
+
Sub-apps may register their own action IDs via `siteInjections.actions`
|
|
133
|
+
on `OrchestratedPortal`. Those IDs become callable through the same
|
|
134
|
+
`requestAction` / postMessage contract as built-ins. Unlike built-ins they
|
|
135
|
+
are not guaranteed to be present on every portal — always probe
|
|
136
|
+
`getAvailableActions()` before using them.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Result shape
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
type ActionRequestResult = {
|
|
144
|
+
status:
|
|
145
|
+
| 'success' // action completed (sync actions may carry a value; capture overlays always do)
|
|
146
|
+
| 'cancelled' // user dismissed the overlay without completing
|
|
147
|
+
| 'unavailable' // unknown action or capability not present on this host
|
|
148
|
+
| 'error'; // action threw
|
|
149
|
+
value?: unknown;
|
|
150
|
+
error?: string;
|
|
151
|
+
requestId?: string; // echoed on postMessage replies
|
|
152
|
+
};
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
All actions now resolve to one of these four terminal statuses. There is no
|
|
156
|
+
`triggered` intermediate status — capture-mode overlays (QR, barcode, NFC)
|
|
157
|
+
keep the caller's promise pending until the user completes the scan or
|
|
158
|
+
dismisses the overlay.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Scanner actions in detail
|
|
163
|
+
|
|
164
|
+
QR, barcode, and NFC scanner actions open the portal's scanner overlay and
|
|
165
|
+
**await user input** — the promise (or postMessage reply) is held open until
|
|
166
|
+
the user either completes a scan or dismisses the overlay.
|
|
167
|
+
|
|
168
|
+
By default the portal also performs its standard navigation after a
|
|
169
|
+
successful scan (e.g. resolving a SmartLinks proof URL). Pass
|
|
170
|
+
`suppressNavigation: true` when you want the raw value and no portal routing.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// Container / widget — get the scanned value, skip portal navigation
|
|
174
|
+
const result = await portal.requestAction('__qrScanner', {
|
|
175
|
+
suppressNavigation: true,
|
|
176
|
+
})
|
|
177
|
+
if (result.status === 'success') {
|
|
178
|
+
const { text, format, resolved } = result.value as QrScanResult
|
|
179
|
+
// text — raw scanned string
|
|
180
|
+
// format — e.g. 'QR_CODE', 'EAN_13'
|
|
181
|
+
// resolved — if the text decoded to a SmartLinks URL:
|
|
182
|
+
// { collectionId, productId?, proofId? }
|
|
183
|
+
}
|
|
184
|
+
if (result.status === 'cancelled') {
|
|
185
|
+
// user closed the scanner without scanning
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
// Container / widget — let the portal navigate AND get the value
|
|
191
|
+
const result = await portal.requestAction('__qrScanner')
|
|
192
|
+
// portal navigates to the scanned link; sub-app also gets the value
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// Iframe — same contract via postMessage
|
|
197
|
+
const requestId = crypto.randomUUID()
|
|
198
|
+
|
|
199
|
+
window.addEventListener('message', function handler(ev) {
|
|
200
|
+
const d = ev.data
|
|
201
|
+
if (!d?._smartlinksIframeMessage) return
|
|
202
|
+
if (d.type !== 'smartlinks:action:result') return
|
|
203
|
+
if (d.payload?.requestId !== requestId) return
|
|
204
|
+
window.removeEventListener('message', handler)
|
|
205
|
+
|
|
206
|
+
if (d.payload.status === 'success') {
|
|
207
|
+
const { text, format, resolved } = d.payload.value
|
|
208
|
+
// use the scan result
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
window.parent.postMessage({
|
|
213
|
+
_smartlinksIframeMessage: true,
|
|
214
|
+
type: 'smartlinks:action:request',
|
|
215
|
+
payload: {
|
|
216
|
+
requestId,
|
|
217
|
+
actionId: '__qrScanner',
|
|
218
|
+
params: { suppressNavigation: true },
|
|
219
|
+
},
|
|
220
|
+
}, '*')
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Scan value types
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
type QrScanResult = {
|
|
227
|
+
text: string
|
|
228
|
+
format: string // 'QR_CODE', 'EAN_13', etc.
|
|
229
|
+
resolved?: {
|
|
230
|
+
collectionId: string
|
|
231
|
+
productId?: string
|
|
232
|
+
proofId?: string
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
type BarcodeScanResult = {
|
|
237
|
+
text: string
|
|
238
|
+
format: string
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
type NfcScanResult = {
|
|
242
|
+
serial: string
|
|
243
|
+
payload?: string
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Relationship to `LinkTarget`
|
|
250
|
+
|
|
251
|
+
Linkpage-style UIs persist a `LinkTarget` of kind `'action'`:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
type ActionLinkTarget = {
|
|
255
|
+
kind: 'action';
|
|
256
|
+
actionId: string;
|
|
257
|
+
params?: Record<string, unknown>;
|
|
258
|
+
};
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
At runtime, resolve it through the portal:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
await portal?.requestAction(target.actionId, target.params);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Never persist a resolved URL when an action is intended — the action
|
|
268
|
+
contract may change platform-side, the persisted ID won't.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Anti-patterns
|
|
273
|
+
|
|
274
|
+
❌ Calling `navigator.share` / `clipboard.writeText` directly from a
|
|
275
|
+
sub-app when `__share` / `__copyLink` exist — you lose host-level
|
|
276
|
+
error handling and capability negotiation.
|
|
277
|
+
|
|
278
|
+
❌ Re-implementing logout by clearing your own state. Always go through
|
|
279
|
+
`__logout` (or `useAuth().logout()` in-tree) so the whole portal
|
|
280
|
+
session ends cleanly.
|
|
281
|
+
|
|
282
|
+
❌ Hard-coding action IDs the host might not support. Probe
|
|
283
|
+
`getAvailableActions()` or the capabilities postMessage first.
|
|
284
|
+
|
|
285
|
+
❌ Reusing a single `requestId` across postMessage calls.
|
|
286
|
+
|
|
287
|
+
❌ Not awaiting the result of capture-mode scanner actions. The promise
|
|
288
|
+
stays pending until the user scans or cancels — fire and forget will
|
|
289
|
+
leak the listener.
|