@proveanything/smartlinks 1.14.12 → 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 +1 -1
- package/dist/docs/portal-request-action.md +226 -387
- package/docs/API_SUMMARY.md +1 -1
- package/docs/portal-request-action.md +226 -387
- package/package.json +1 -1
|
@@ -1,450 +1,289 @@
|
|
|
1
|
-
|
|
1
|
+
# Requesting Built-in Portal Actions
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
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.
|
|
5
12
|
|
|
13
|
+
---
|
|
6
14
|
|
|
15
|
+
## Why this exists
|
|
7
16
|
|
|
8
|
-
|
|
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.
|
|
9
22
|
|
|
23
|
+
---
|
|
10
24
|
|
|
25
|
+
## Container / Widget Apps (Same React Tree)
|
|
11
26
|
|
|
12
|
-
|
|
27
|
+
```tsx
|
|
28
|
+
import { usePortalActions } from '@proveanything/portal-framework';
|
|
13
29
|
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
```
|
|
16
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
|
+
```
|
|
17
53
|
|
|
18
|
-
|
|
19
|
-
|
|
54
|
+
Combine with `BUILTIN_ACTION_META` (also exported) for labels/icons in
|
|
55
|
+
your action pickers.
|
|
20
56
|
|
|
21
|
-
|
|
22
|
-
Sub-apps stop owning auth UI. They only ever declare intent:
|
|
57
|
+
---
|
|
23
58
|
|
|
24
|
-
|
|
59
|
+
## Iframe Apps (Cross-Origin)
|
|
25
60
|
|
|
26
|
-
|
|
61
|
+
```ts
|
|
62
|
+
const requestId = crypto.randomUUID();
|
|
27
63
|
|
|
28
|
-
|
|
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);
|
|
29
70
|
|
|
30
|
-
|
|
31
|
-
|
|
71
|
+
const { status, value, error } = d.payload;
|
|
72
|
+
// …handle result
|
|
73
|
+
});
|
|
32
74
|
|
|
33
75
|
window.parent.postMessage({
|
|
34
76
|
_smartlinksIframeMessage: true,
|
|
35
|
-
type: 'smartlinks:
|
|
77
|
+
type: 'smartlinks:action:request',
|
|
36
78
|
payload: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
requestId: string, // so we can reply
|
|
79
|
+
requestId,
|
|
80
|
+
actionId: '__copyLink',
|
|
81
|
+
params: { url: window.location.href },
|
|
41
82
|
},
|
|
42
83
|
}, '*');
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{ type: 'smartlinks:authkit:login-result',
|
|
46
|
-
payload: { requestId, status: 'success' | 'cancelled' | 'error', user?, token? } }
|
|
47
|
-
For container/widget apps (same React tree), expose the same thing as a hook so they don't need postMessage:
|
|
48
|
-
|
|
49
|
-
const { requestLogin } = useSafeAuth();
|
|
50
|
-
const result = await requestLogin({ reason: 'bid' });
|
|
51
|
-
if (result.status === 'success') { /* place bid */ }
|
|
52
|
-
State restoration — the important part
|
|
53
|
-
You're right that this is what makes or breaks the UX. Options, in increasing order of robustness:
|
|
54
|
-
|
|
55
|
-
Do nothing special — AuthKit flows that stay in-page (modal email/password, in-modal OTP) just work; the sub-app never unmounted, its URL/state is intact.
|
|
56
|
-
For redirect flows (magic link, WhatsApp deep-link, OAuth bounce) — portal snapshots the full URL (including the sub-app's hash/query) into sessionStorage keyed by requestId before redirecting, and restores it on return. The sub-app gets remounted at the exact same deep-link.
|
|
57
|
-
Resume callback — after restoration, portal re-posts the original request-login requestId to the sub-app as login-result: success so it can resume its specific action (e.g. auto-submit the bid the user was about to place).
|
|
58
|
-
I'd build all three; they're additive.
|
|
59
|
-
|
|
60
|
-
Config flexibility you mentioned
|
|
61
|
-
Per-collection (in layout config) or per-request:
|
|
84
|
+
```
|
|
62
85
|
|
|
63
|
-
|
|
64
|
-
signupProminence: 'primary' | 'secondary' | 'hidden' (we already have this type)
|
|
65
|
-
methods?: ('email'|'phone'|'oauth')[] — restrict
|
|
66
|
-
Copy override per reason (e.g. "Sign in to place a bid")
|
|
67
|
-
What we'd remove / deprecate
|
|
68
|
-
The current pattern where sub-apps import smartlinks-auth-ui and call login() themselves becomes the exception, only for genuinely custom auth (your bidder API example). Standard "I just need a logged-in user" goes through requestLogin.
|
|
69
|
-
The smartlinks:authkit:login / :logout broadcast messages stay (for the custom-flow case), but most apps stop using them.
|
|
70
|
-
Open questions before I build
|
|
71
|
-
Scope — implement on portal-framework side (modal + message handler + hook) and document the contract for sub-apps? Or do you also want me to update the two problem apps (auction, claim) in this repo? (I don't think they live here, but confirming.)
|
|
72
|
-
Redirect-flow state restoration — do any of your AuthKit methods currently redirect away from the page? (Magic link click-through, OAuth.) If yes, I'll wire the sessionStorage snapshot/restore. If everything is in-modal today, I'll stub it and we add it when needed.
|
|
73
|
-
requestLogin for container/widget apps — add it to useSafeAuth() return value, or a separate hook useRequestLogin()? I lean toward folding into useSafeAuth so there's one entry point.
|
|
74
|
-
If you confirm those three, I'll implement it.
|
|
86
|
+
### Capabilities probe
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
```
|
|
78
97
|
|
|
98
|
+
Use this to hide actions the host doesn't support (e.g. `__nfcScanner`
|
|
99
|
+
on iOS web).
|
|
79
100
|
|
|
80
|
-
|
|
81
|
-
Centralized auth modal flow
|
|
82
|
-
Done.
|
|
101
|
+
---
|
|
83
102
|
|
|
84
|
-
|
|
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.
|
|
85
127
|
|
|
86
|
-
|
|
87
|
-
smartlinks:authkit:request-login postMessage with matching smartlinks:authkit:login-result reply (carrying requestId) for iframe apps.
|
|
88
|
-
Portal owns the modal (still the same AuthModal → SmartlinksAuthUI, so all your existing AuthKit redirect handling — WhatsApp, OAuth, magic link — works unchanged).
|
|
89
|
-
Header login button now goes through the same requestLogin path, so there's one code path for everything.
|
|
90
|
-
Already-authenticated callers resolve immediately without showing the modal.
|
|
91
|
-
Docs updated in packages/portal-framework/docs/authentication.md with the new recommended pattern and the legacy "publish your own session" pattern explicitly flagged as the exception.
|
|
92
|
-
Sub-app authors can now delete their bundled auth UI entirely; the auction/claim apps just await auth.requestLogin({ reason: 'bid' }).
|
|
128
|
+
---
|
|
93
129
|
|
|
94
|
-
|
|
95
|
-
Okay, so we need to document this. Um, we need to document it in the Smart Links SDK and, um, if you think there needs to be any additional document in the core workspace memory, let me know. Um, but basically, can you give me a-- If you can find the right documents, can you give me a new and updated version of any documents in the Smart Links SDK that'll help describe this?
|
|
130
|
+
## App-registered actions
|
|
96
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.
|
|
97
137
|
|
|
98
|
-
|
|
99
|
-
Added portal-request-login doc
|
|
100
|
-
Two SDK docs are ready to drop into node_modules/@proveanything/smartlinks/docs/:
|
|
138
|
+
---
|
|
101
139
|
|
|
102
|
-
|
|
103
|
-
sdk-doc-additions/portal-auth-broadcast.md (updated) — Now opens with a pointer to portal-request-login.md and is explicitly scoped to the custom-auth case (sub-app runs its own flow and publishes the resulting session). The login/logout postMessage payloads are unchanged.
|
|
104
|
-
When you copy them into the SDK, also add two rows to API_SUMMARY.md under an "AuthKit / Portal Bridge" section (it doesn't exist yet):
|
|
140
|
+
## Result shape
|
|
105
141
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
```
|
|
110
154
|
|
|
111
|
-
|
|
112
|
-
|
|
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.
|
|
113
159
|
|
|
160
|
+
---
|
|
114
161
|
|
|
115
|
-
|
|
116
|
-
Read
|
|
117
|
-
builtin-actions.ts
|
|
118
|
-
Yes — this is a very natural extension and worth doing. The plumbing already exists; you'd just be exposing it through a programmatic channel instead of only navigation zones.
|
|
162
|
+
## Scanner actions in detail
|
|
119
163
|
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
122
167
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
126
171
|
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
```
|
|
129
188
|
|
|
130
|
-
|
|
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
|
+
```
|
|
131
194
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
})
|
|
137
211
|
|
|
138
212
|
window.parent.postMessage({
|
|
139
213
|
_smartlinksIframeMessage: true,
|
|
140
214
|
type: 'smartlinks:action:request',
|
|
141
|
-
payload: {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
__qrScanner / __barcodeScanner { text, format, resolved?: { collectionId, productId?, proofId? } }
|
|
149
|
-
__nfcScanner { serial, payload? }
|
|
150
|
-
__share { method: 'native' | 'clipboard' }
|
|
151
|
-
__language { language }
|
|
152
|
-
__account (login) / __profile / __settings { user? }
|
|
153
|
-
__refresh {}
|
|
154
|
-
__logout (new) {}
|
|
155
|
-
__takePhoto (proposed) { dataUrl } or { assetId }
|
|
156
|
-
Why this is the right shape
|
|
157
|
-
It's the same primitive. requestLogin becomes literally requestAction('__account', { mode }) under the hood — you keep the dedicated requestLogin as a typed convenience and document it as such.
|
|
158
|
-
One layout-builder source of truth. BUILTIN_ACTION_META already drives the Linkpage-style picker. The same enum becomes the dropdown in any "pick an action" UI.
|
|
159
|
-
Capability negotiation comes free. getAvailableActions() (and a capabilities field in the iframe context sync) lets Linkpage hide "NFC scan" on iOS web, "take photo" without a camera, "logout" when not logged in, etc. — much cleaner than every app re-detecting.
|
|
160
|
-
Mobile shell maps onto the same enum. When the Capacitor host wants to override __qrScanner with the native camera, it registers the same action ID — sub-apps don't change.
|
|
161
|
-
What I'd add to the set
|
|
162
|
-
Genuinely missing today:
|
|
163
|
-
|
|
164
|
-
__logout — symmetric with login, currently no clean way to trigger.
|
|
165
|
-
__takePhoto — distinct from QR scan; returns image/asset.
|
|
166
|
-
__pickAsset — opens the same AssetPicker the admin UI uses (useful for any "let the user pick a saved image" flow).
|
|
167
|
-
__copyLink — distinct from __share; deterministic clipboard with toast.
|
|
168
|
-
__navigate — wraps SL.navigation.resolveLink(linkTarget).navigate(); lets iframes use the portal's link resolver without re-implementing it.
|
|
169
|
-
I'd not add: file download, generic toast, dialog. Those are app concerns, not portal capabilities.
|
|
170
|
-
|
|
171
|
-
What to be careful about
|
|
172
|
-
Keep the surface closed. Only __-prefixed IDs the framework ships. Don't let apps register their own — that's what apps and LinkTarget's 'app' / 'deep' kinds are for. The action registry is for host capabilities.
|
|
173
|
-
Permissioning. __logout from a random iframe should probably require user confirmation; __settings exposing account deletion definitely should. Decide per-action whether the portal prompts.
|
|
174
|
-
Linkpage UX vs programmatic. Linkpage's links should resolve through the same requestAction path so a link configured as "open QR scanner" behaves identically to a programmatic call. Don't fork two execution paths.
|
|
175
|
-
Don't replace LinkTarget. LinkTarget stays the persisted shape (it's what's saved in config). requestAction is the runtime API. Linkpage stores a LinkTarget of a new kind: { kind: 'action'; actionId; params? } — and the resolver dispatches that to requestAction.
|
|
176
|
-
Recommendation
|
|
177
|
-
Build it in two phases:
|
|
178
|
-
|
|
179
|
-
Phase 1 — programmatic API + iframe contract + LinkTarget 'action' kind. Wire requestAction to the existing built-in actions (no new actions yet). Update Linkpage to use it.
|
|
180
|
-
Phase 2 — add __logout, __takePhoto, __pickAsset, __copyLink, __navigate. Document return shapes in the SDK.
|
|
181
|
-
Want me to write up a plan for Phase 1, or go straight to implementing it?
|
|
182
|
-
|
|
183
|
-
19 May at 09:46
|
|
184
|
-
Um, great. Let's go and add these. This is great
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
Thought for 7s
|
|
188
|
-
Added sync actions & doc
|
|
189
|
-
Add requestAction core API
|
|
190
|
-
Add new built-in actions
|
|
191
|
-
Extend LinkTarget with action kind
|
|
192
|
-
Document requestAction in SDK docs
|
|
193
|
-
Shipped the requestAction contract end-to-end:
|
|
194
|
-
|
|
195
|
-
ActionRequestProvider (packages/portal-framework/src/orchestrator/ActionRequestProvider.tsx) — usePortalActions().requestAction(actionId, params) + iframe postMessage bridge (smartlinks:action:request / :result) + capabilities probe (smartlinks:action:capabilities).
|
|
196
|
-
Synchronous built-ins executed inline: __refresh, __share, __copyLink, __navigate, __login, __logout, __account (adaptive). Overlay built-ins (__qrScanner, __profile, etc.) dispatch through SiteInjectionContext.triggerAction and resolve triggered for now.
|
|
197
|
-
BUILTIN_ACTION_META extended with LOGIN, LOGOUT, COPY_LINK, NAVIGATE entries so Linkpage-style pickers list them automatically.
|
|
198
|
-
Mounted inside SiteInjectionProvider in both OrchestratedPortal branches, exported from the framework root.
|
|
199
|
-
portal-request-action.md — full SDK doc covering hook + iframe contract, capability discovery, the LinkTarget 'action' kind, and anti-patterns.
|
|
200
|
-
Returning values from overlay actions (e.g. QR text back to the caller) is the natural next pass — the envelope already supports value; only the overlay components need a resolve() callback wired in.
|
|
201
|
-
|
|
202
|
-
19 May at 11:37
|
|
203
|
-
Okay, so can you take a step back for a second? 'Cause I'm getting confused about all the docs. So I now have four different docs that all say SDK doc additions. Can you tell me which ones I actually still need? Which ones are already in the SDK? Which ones have been supplemented? Can we delete the ones we no longer need, look at the SDK and check everything's right, and give me a single summary o ...
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
Thought for 3s
|
|
207
|
-
Used 5 tools
|
|
208
|
-
|
|
209
|
-
Here's the audit:
|
|
210
|
-
|
|
211
|
-
What's where
|
|
212
|
-
File in sdk-doc-additions/ In installed SDK? Status
|
|
213
|
-
portal-auth-broadcast.md (158 lines) Yes, older 136-line version Supersedes — our copy adds the pointer to requestLogin and reframes this as the custom-auth-only path
|
|
214
|
-
portal-back-button.md (187 lines) Yes, older 95-line version Supersedes — our copy is the expanded version from the earlier back-button work
|
|
215
|
-
portal-request-login.md (240 lines) No New — missing from SDK
|
|
216
|
-
portal-request-action.md (194 lines) No New — missing from SDK
|
|
217
|
-
API_SUMMARY.md in the SDK has no "AuthKit / Portal Bridge" section yet — none of the four docs are linked from it.
|
|
218
|
-
|
|
219
|
-
Recommendation
|
|
220
|
-
Keep all four. None are redundant — the two that exist in the SDK are stale versions that need replacing, and the other two are net-new. Deleting any of them would lose content the SDK doesn't have yet.
|
|
221
|
-
|
|
222
|
-
What's actually missing from the SDK right now
|
|
223
|
-
Replace docs/portal-auth-broadcast.md with our 158-line revision (adds the "read requestLogin first" pointer and scopes this doc to custom-auth flows only).
|
|
224
|
-
Replace docs/portal-back-button.md with our 187-line revision.
|
|
225
|
-
Add docs/portal-request-login.md (canonical login contract).
|
|
226
|
-
Add docs/portal-request-action.md (general-purpose built-in actions contract).
|
|
227
|
-
Add to API_SUMMARY.md a new "AuthKit / Portal Bridge" section with rows for all four docs (currently no entries point to any of them).
|
|
228
|
-
Want me to also verify our portal-back-button.md revision against the implementation in OrchestratedPortal / useNavigationState, or leave it as-is?
|
|
229
|
-
|
|
230
|
-
Security
|
|
231
|
-
2
|
|
232
|
-
Errors (outdated)
|
|
233
|
-
Code
|
|
234
|
-
Ask Lovable...
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
Add files
|
|
238
|
-
Drop any files here to add them to your message
|
|
239
|
-
|
|
240
|
-
Search code
|
|
241
|
-
.github/workflows
|
|
242
|
-
sync-package-lock.yml
|
|
243
|
-
.lovable
|
|
244
|
-
plan.md
|
|
245
|
-
deploy/portal
|
|
246
|
-
server
|
|
247
|
-
cloudbuild.yaml
|
|
248
|
-
Dockerfile
|
|
249
|
-
nginx.conf
|
|
250
|
-
package.json
|
|
251
|
-
README.md
|
|
252
|
-
tsconfig.server.json
|
|
253
|
-
vite.portal.config.ts
|
|
254
|
-
packages/portal-framework
|
|
255
|
-
docs
|
|
256
|
-
sdk-doc-additions
|
|
257
|
-
portal-auth-broadcast.md
|
|
258
|
-
portal-back-button.md
|
|
259
|
-
portal-request-action.md
|
|
260
|
-
portal-request-login.md
|
|
261
|
-
ai-assistant.md
|
|
262
|
-
ai-content-extraction.md
|
|
263
|
-
architecture.md
|
|
264
|
-
authentication.md
|
|
265
|
-
cross-app-communication.md
|
|
266
|
-
direct-component-rendering.md
|
|
267
|
-
embed-mode.md
|
|
268
|
-
routing.md
|
|
269
|
-
site-injection.md
|
|
270
|
-
src
|
|
271
|
-
GETTING_STARTED.md
|
|
272
|
-
package.json
|
|
273
|
-
README.md
|
|
274
|
-
rollup.config.js
|
|
275
|
-
tsconfig.build.json
|
|
276
|
-
tsconfig.json
|
|
277
|
-
public
|
|
278
|
-
app.admin.json
|
|
279
|
-
app.manifest.json
|
|
280
|
-
robots.txt
|
|
281
|
-
scripts
|
|
282
|
-
check-lockfile-sync.mjs
|
|
283
|
-
src
|
|
284
|
-
components
|
|
285
|
-
docs
|
|
286
|
-
hooks
|
|
287
|
-
i18n
|
|
288
|
-
integrations/supabase
|
|
289
|
-
lib
|
|
290
|
-
pages
|
|
291
|
-
types
|
|
292
|
-
utils
|
|
293
|
-
widgets
|
|
294
|
-
App.css
|
|
295
|
-
App.tsx
|
|
296
|
-
index.css
|
|
297
|
-
main.tsx
|
|
298
|
-
platform.ts
|
|
299
|
-
preview-main.tsx
|
|
300
|
-
PreviewApp.tsx
|
|
301
|
-
vite-env.d.ts
|
|
302
|
-
supabase
|
|
303
|
-
config.toml
|
|
304
|
-
.dockerignore
|
|
305
|
-
.env
|
|
306
|
-
.env.example
|
|
307
|
-
.gitignore
|
|
308
|
-
bun.lock
|
|
309
|
-
components.json
|
|
310
|
-
eslint.config.js
|
|
311
|
-
index.html
|
|
312
|
-
package.json
|
|
313
|
-
postcss.config.js
|
|
314
|
-
preview.html
|
|
315
|
-
tailwind.config.ts
|
|
316
|
-
tsconfig.app.json
|
|
317
|
-
tsconfig.json
|
|
318
|
-
tsconfig.node.json
|
|
319
|
-
vite.config.ts
|
|
320
|
-
vite.config.widget.ts
|
|
321
|
-
packages/portal-framework/docs/sdk-doc-additions/portal-auth-broadcast.md
|
|
322
|
-
packages/portal-framework/docs/sdk-doc-additions/portal-back-button.md
|
|
323
|
-
packages/portal-framework/docs/sdk-doc-additions/portal-request-login.md
|
|
324
|
-
packages/portal-framework/docs/sdk-doc-additions/portal-request-action.md
|
|
325
|
-
1
|
|
326
|
-
2
|
|
327
|
-
3
|
|
328
|
-
4
|
|
329
|
-
5
|
|
330
|
-
6
|
|
331
|
-
7
|
|
332
|
-
8
|
|
333
|
-
9
|
|
334
|
-
10
|
|
335
|
-
11
|
|
336
|
-
12
|
|
337
|
-
13
|
|
338
|
-
14
|
|
339
|
-
15
|
|
340
|
-
16
|
|
341
|
-
17
|
|
342
|
-
18
|
|
343
|
-
19
|
|
344
|
-
20
|
|
345
|
-
21
|
|
346
|
-
22
|
|
347
|
-
23
|
|
348
|
-
24
|
|
349
|
-
25
|
|
350
|
-
26
|
|
351
|
-
27
|
|
352
|
-
28
|
|
353
|
-
29
|
|
354
|
-
30
|
|
355
|
-
31
|
|
356
|
-
32
|
|
357
|
-
33
|
|
358
|
-
34
|
|
359
|
-
35
|
|
360
|
-
36
|
|
361
|
-
37
|
|
362
|
-
38
|
|
363
|
-
39
|
|
364
|
-
40
|
|
365
|
-
41
|
|
366
|
-
42
|
|
367
|
-
43
|
|
368
|
-
44
|
|
369
|
-
45
|
|
370
|
-
46
|
|
371
|
-
47
|
|
372
|
-
48
|
|
373
|
-
49
|
|
374
|
-
50
|
|
375
|
-
51
|
|
376
|
-
52
|
|
377
|
-
53
|
|
378
|
-
54
|
|
379
|
-
55
|
|
380
|
-
56
|
|
381
|
-
57
|
|
382
|
-
58
|
|
383
|
-
59
|
|
384
|
-
60
|
|
385
|
-
61
|
|
386
|
-
62
|
|
387
|
-
63
|
|
388
|
-
# Requesting Built-in Portal Actions
|
|
389
|
-
|
|
390
|
-
> **Where to put this file:** copy into the SmartLinks SDK at
|
|
391
|
-
> `node_modules/@proveanything/smartlinks/docs/portal-request-action.md`
|
|
392
|
-
> and add a row to `API_SUMMARY.md`'s "AuthKit / Portal Bridge" section.
|
|
393
|
-
|
|
394
|
-
`requestAction` is the general-purpose sibling of
|
|
395
|
-
[`requestLogin`](./portal-request-login.md). It lets a sub-app
|
|
396
|
-
(container, widget, or iframe) trigger any of the portal's built-in
|
|
397
|
-
capabilities — open the QR scanner, log out, copy the current link,
|
|
398
|
-
navigate to another app — and await a result.
|
|
215
|
+
payload: {
|
|
216
|
+
requestId,
|
|
217
|
+
actionId: '__qrScanner',
|
|
218
|
+
params: { suppressNavigation: true },
|
|
219
|
+
},
|
|
220
|
+
}, '*')
|
|
221
|
+
```
|
|
399
222
|
|
|
400
|
-
|
|
401
|
-
portal's standardized capabilities. The portal owns the implementation;
|
|
402
|
-
your sub-app declares intent.
|
|
223
|
+
### Scan value types
|
|
403
224
|
|
|
404
|
-
|
|
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
|
+
}
|
|
405
235
|
|
|
406
|
-
|
|
236
|
+
type BarcodeScanResult = {
|
|
237
|
+
text: string
|
|
238
|
+
format: string
|
|
239
|
+
}
|
|
407
240
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
241
|
+
type NfcScanResult = {
|
|
242
|
+
serial: string
|
|
243
|
+
payload?: string
|
|
244
|
+
}
|
|
245
|
+
```
|
|
413
246
|
|
|
414
247
|
---
|
|
415
248
|
|
|
416
|
-
##
|
|
249
|
+
## Relationship to `LinkTarget`
|
|
417
250
|
|
|
418
|
-
|
|
419
|
-
import { usePortalActions } from '@proveanything/portal-framework';
|
|
251
|
+
Linkpage-style UIs persist a `LinkTarget` of kind `'action'`:
|
|
420
252
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
});
|
|
428
|
-
// result.status === 'success' | 'cancelled' | 'unavailable' | 'error' | 'triggered'
|
|
429
|
-
}
|
|
430
|
-
return <button onClick={onClick}>Share</button>;
|
|
431
|
-
}
|
|
253
|
+
```ts
|
|
254
|
+
type ActionLinkTarget = {
|
|
255
|
+
kind: 'action';
|
|
256
|
+
actionId: string;
|
|
257
|
+
params?: Record<string, unknown>;
|
|
258
|
+
};
|
|
432
259
|
```
|
|
433
260
|
|
|
434
|
-
|
|
435
|
-
mounted upstream — treat that the same as `status: 'unavailable'`.
|
|
436
|
-
|
|
437
|
-
### Discovering available actions
|
|
261
|
+
At runtime, resolve it through the portal:
|
|
438
262
|
|
|
439
263
|
```ts
|
|
440
|
-
|
|
441
|
-
// → ['__refresh','__share','__copyLink','__navigate','__login','__logout',
|
|
442
|
-
// '__account','__qrScanner','__barcodeScanner','__nfcScanner', ...]
|
|
264
|
+
await portal?.requestAction(target.actionId, target.params);
|
|
443
265
|
```
|
|
444
266
|
|
|
445
|
-
|
|
446
|
-
|
|
267
|
+
Never persist a resolved URL when an action is intended — the action
|
|
268
|
+
contract may change platform-side, the persisted ID won't.
|
|
447
269
|
|
|
448
270
|
---
|
|
449
271
|
|
|
450
|
-
##
|
|
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.
|