@proveanything/smartlinks 1.14.10 → 1.14.12

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.
@@ -0,0 +1,236 @@
1
+ # Requesting Login from the Portal
2
+
3
+ This is the **recommended** way for a micro-app to get a logged-in user. The
4
+ sub-app does not render any auth UI of its own — it asks the portal to run
5
+ its standard AuthKit flow and awaits a result. The portal owns the modal,
6
+ WhatsApp / OAuth / magic-link redirects, session persistence, and the
7
+ header avatar swap.
8
+
9
+ Use `requestLogin` whenever the answer to "what do I want?" is simply
10
+ *"a logged-in user before I continue"*. Only fall back to
11
+ [publishing your own session](./portal-auth-broadcast.md) when the app runs
12
+ a genuinely custom auth flow (e.g. its own bidder API).
13
+
14
+ ---
15
+
16
+ ## Why this exists
17
+
18
+ Historically every sub-app imported `@proveanything/smartlinks-auth-ui` and
19
+ rendered its own login modal. That meant:
20
+
21
+ - Two React copies of `AuthContext` could exist at once → silent session
22
+ desync between the header and the sub-app.
23
+ - Each app re-implemented the same WhatsApp / OAuth / magic-link flow.
24
+ - Redirect bounces would land back on the portal with no memory of which
25
+ sub-app had triggered the login.
26
+
27
+ The portal now owns the entire flow. Sub-apps just declare intent.
28
+
29
+ ---
30
+
31
+ ## Container / Widget Apps (Same React Tree)
32
+
33
+ These share the portal's React context. Use `useSafeAuth()` from the
34
+ portal framework:
35
+
36
+ ```tsx
37
+ import { useSafeAuth } from '@proveanything/portal-framework';
38
+
39
+ function PlaceBidButton() {
40
+ const auth = useSafeAuth();
41
+
42
+ async function onClick() {
43
+ const result = await auth?.requestLogin?.({ reason: 'bid' });
44
+ if (result?.status === 'success' || result?.status === 'already-authenticated') {
45
+ // auth.user / auth.token are now populated.
46
+ await api.placeBid(...);
47
+ }
48
+ // 'cancelled' → user dismissed the modal.
49
+ // 'unavailable' → portal has no AuthKit configured.
50
+ }
51
+
52
+ return <button onClick={onClick}>Place bid</button>;
53
+ }
54
+ ```
55
+
56
+ `requestLogin` is also exposed as `useAuthRequest().requestLogin` if you
57
+ prefer a dedicated hook, but the folded `useSafeAuth()` shape is preferred
58
+ so callers have one entry point for both session reads and login intent.
59
+
60
+ ### Options
61
+
62
+ ```ts
63
+ auth.requestLogin({
64
+ reason?: string, // analytics / copy hint
65
+ mode?: 'login' | 'signup' | 'either', // default 'either'
66
+ signupProminence?: SignupProminence, // overrides modal default
67
+ resolveIfAuthenticated?: boolean, // default true
68
+ });
69
+ ```
70
+
71
+ | Option | Default | Effect |
72
+ |---|---|---|
73
+ | `reason` | — | Free-form tag. Surfaced to analytics; some templates use it for copy. |
74
+ | `mode` | `'either'` | `'signup'` biases the modal to the registration tab. |
75
+ | `signupProminence` | portal default | One of `'minimal' | 'balanced' | 'emphasized'`. |
76
+ | `resolveIfAuthenticated` | `true` | When already logged in, resolve immediately without showing the modal. Set `false` to force a fresh prompt (e.g. re-auth before a sensitive action). |
77
+
78
+ ### Result
79
+
80
+ ```ts
81
+ type LoginRequestResult = {
82
+ status: 'success' | 'already-authenticated' | 'cancelled' | 'unavailable';
83
+ user?: PortalAuthBridgeUser | null;
84
+ token?: string | null;
85
+ };
86
+ ```
87
+
88
+ - `success` — fresh login completed inside the modal.
89
+ - `already-authenticated` — there was already a session; nothing was shown.
90
+ - `cancelled` — user dismissed the modal without logging in.
91
+ - `unavailable` — no AuthKit is configured on this portal. Treat as
92
+ not-logged-in and surface an appropriate message; the user cannot log
93
+ in from this portal at all.
94
+
95
+ ---
96
+
97
+ ## Iframe Apps (Cross-Origin)
98
+
99
+ Iframe apps don't share React context with the portal. They post a request
100
+ message and listen for a single correlated reply:
101
+
102
+ ```ts
103
+ const requestId = crypto.randomUUID();
104
+
105
+ window.addEventListener('message', function handler(ev) {
106
+ const d = ev.data;
107
+ if (!d || d._smartlinksIframeMessage !== true) return;
108
+ if (d.type !== 'smartlinks:authkit:login-result') return;
109
+ if (d.payload?.requestId !== requestId) return;
110
+ window.removeEventListener('message', handler);
111
+
112
+ const { status, user, token } = d.payload;
113
+ if (status === 'success' || status === 'already-authenticated') {
114
+ // user + token populated — resume the action that needed auth
115
+ placeBid();
116
+ }
117
+ });
118
+
119
+ window.parent.postMessage({
120
+ _smartlinksIframeMessage: true,
121
+ type: 'smartlinks:authkit:request-login',
122
+ payload: {
123
+ requestId,
124
+ reason: 'bid',
125
+ mode: 'either',
126
+ resolveIfAuthenticated: true,
127
+ },
128
+ }, '*');
129
+ ```
130
+
131
+ The portal opens its modal, runs the standard AuthKit flow (including any
132
+ WhatsApp / OAuth / magic-link redirect bounces), then posts exactly one
133
+ `login-result` back with the same `requestId`.
134
+
135
+ ### Message contract
136
+
137
+ **Request — child → parent**
138
+
139
+ ```ts
140
+ {
141
+ _smartlinksIframeMessage: true,
142
+ type: 'smartlinks:authkit:request-login',
143
+ payload: {
144
+ requestId: string, // generate per request; echoed in the reply
145
+ reason?: string,
146
+ mode?: 'login' | 'signup' | 'either',
147
+ signupProminence?: 'minimal' | 'balanced' | 'emphasized',
148
+ resolveIfAuthenticated?: boolean,
149
+ }
150
+ }
151
+ ```
152
+
153
+ **Reply — parent → child**
154
+
155
+ ```ts
156
+ {
157
+ _smartlinksIframeMessage: true,
158
+ type: 'smartlinks:authkit:login-result',
159
+ payload: {
160
+ requestId: string, // matches the request
161
+ status: 'success' | 'already-authenticated' | 'cancelled' | 'unavailable',
162
+ user?: { uid, email?, displayName?, photoURL?, phoneNumber?, accountData? } | null,
163
+ token?: string | null,
164
+ }
165
+ }
166
+ ```
167
+
168
+ Only one reply is sent per `requestId`. Stacking multiple request-logins
169
+ cancels prior pending requests — they receive `status: 'cancelled'`.
170
+
171
+ ---
172
+
173
+ ## Redirect-Bounce Flows (WhatsApp, OAuth, Magic Link)
174
+
175
+ These are handled **inside the portal's AuthModal by AuthKit itself** —
176
+ your iframe stays mounted at the same URL throughout. There is no
177
+ sub-app state restoration to wire up; the modal closes when the redirect
178
+ resolves and the portal posts back your `login-result`.
179
+
180
+ If you have a sub-app that lives on a route the user navigates away from
181
+ during the redirect, snapshot whatever you need into your own
182
+ `sessionStorage` before calling `requestLogin`, the same as for any other
183
+ async UX. The portal does not (yet) snapshot sub-app state on your behalf.
184
+
185
+ ---
186
+
187
+ ## Reading the Current Session
188
+
189
+ You don't need to call `requestLogin` just to read the current session —
190
+ that comes for free:
191
+
192
+ ```tsx
193
+ // Container / widget
194
+ import { useSafeAuth } from '@proveanything/portal-framework';
195
+ const { user, token, isAuthenticated } = useSafeAuth() ?? {};
196
+ ```
197
+
198
+ ```ts
199
+ // Iframe — the IframeResponder caches the parent's user under `cache.user`.
200
+ // Read via the SDK's standard hooks/helpers.
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Anti-Patterns
206
+
207
+ ❌ Sub-apps importing `@proveanything/smartlinks-auth-ui` and rendering
208
+ their own `<SmartlinksAuthUI />` modal. This is what `requestLogin`
209
+ replaces — duplicate React contexts cause silent header desync.
210
+
211
+ ❌ Posting `request-login` and then also opening a local fallback modal.
212
+ Pick one; the portal modal is canonical.
213
+
214
+ ❌ Reusing a single `requestId` across multiple calls. Generate a new ID
215
+ per request so replies can be correlated unambiguously.
216
+
217
+ ❌ Treating `unavailable` as an error to retry. It means the portal has no
218
+ AuthKit configured — surface a "login isn't available here" message.
219
+
220
+ ❌ Setting `resolveIfAuthenticated: false` for normal "gate this action"
221
+ use cases — you'll re-prompt logged-in users for no reason. Only use
222
+ it for genuine step-up auth.
223
+
224
+ ---
225
+
226
+ ## Relationship to `portal-auth-broadcast.md`
227
+
228
+ | Concern | Use |
229
+ |---|---|
230
+ | "Log the user in so I can call my API" | **`requestLogin`** (this doc) |
231
+ | "I ran my own custom auth flow, here's the resulting session" | [`portal-auth-broadcast.md`](./portal-auth-broadcast.md) |
232
+ | "Log the user out of the whole portal" | `useAuth().logout()` (container/widget) or `smartlinks:authkit:logout` postMessage (iframe) |
233
+
234
+ Both flows ultimately drive the same `PortalAuthProvider.login` /
235
+ `logout` calls and produce the same observable side effects (header
236
+ avatar, `useAuth()` re-render, `setBearerToken` on the SDK).
package/openapi.yaml CHANGED
@@ -2586,6 +2586,113 @@ paths:
2586
2586
  description: Unauthorized
2587
2587
  404:
2588
2588
  description: Not found
2589
+ /admin/collection/{collectionId}/contacts/search:
2590
+ get:
2591
+ tags:
2592
+ - contact
2593
+ summary: contact.search
2594
+ operationId: contact_search
2595
+ security:
2596
+ - bearerAuth: []
2597
+ parameters:
2598
+ - name: collectionId
2599
+ in: path
2600
+ required: true
2601
+ schema:
2602
+ type: string
2603
+ - name: q
2604
+ in: query
2605
+ required: false
2606
+ schema:
2607
+ type: string
2608
+ - name: typeahead
2609
+ in: query
2610
+ required: false
2611
+ schema:
2612
+ type: string
2613
+ - name: email
2614
+ in: query
2615
+ required: false
2616
+ schema:
2617
+ type: string
2618
+ - name: phone
2619
+ in: query
2620
+ required: false
2621
+ schema:
2622
+ type: string
2623
+ - name: id
2624
+ in: query
2625
+ required: false
2626
+ schema:
2627
+ type: string
2628
+ - name: userId
2629
+ in: query
2630
+ required: false
2631
+ schema:
2632
+ type: string
2633
+ - name: source
2634
+ in: query
2635
+ required: false
2636
+ schema:
2637
+ type: string
2638
+ - name: locale
2639
+ in: query
2640
+ required: false
2641
+ schema:
2642
+ type: string
2643
+ - name: createdFrom
2644
+ in: query
2645
+ required: false
2646
+ schema:
2647
+ type: string
2648
+ - name: createdTo
2649
+ in: query
2650
+ required: false
2651
+ schema:
2652
+ type: string
2653
+ - name: externalIdKey
2654
+ in: query
2655
+ required: false
2656
+ schema:
2657
+ type: string
2658
+ - name: externalIdValue
2659
+ in: query
2660
+ required: false
2661
+ schema:
2662
+ type: string
2663
+ - name: customFieldKey
2664
+ in: query
2665
+ required: false
2666
+ schema:
2667
+ type: string
2668
+ - name: customFieldValue
2669
+ in: query
2670
+ required: false
2671
+ schema:
2672
+ type: string
2673
+ - name: limit
2674
+ in: query
2675
+ required: false
2676
+ schema:
2677
+ type: string
2678
+ - name: offset
2679
+ in: query
2680
+ required: false
2681
+ schema:
2682
+ type: string
2683
+ responses:
2684
+ 200:
2685
+ description: Success
2686
+ content:
2687
+ application/json:
2688
+ schema:
2689
+ $ref: "#/components/schemas/ContactSearchResponse"
2690
+ 400:
2691
+ description: Bad request
2692
+ 401:
2693
+ description: Unauthorized
2694
+ 404:
2695
+ description: Not found
2589
2696
  /admin/collection/{collectionId}/contacts/upsert:
2590
2697
  post:
2591
2698
  tags:
@@ -19580,6 +19687,70 @@ components:
19580
19687
  - items
19581
19688
  - limit
19582
19689
  - offset
19690
+ ContactSearchParams:
19691
+ type: object
19692
+ properties:
19693
+ collectionId:
19694
+ type: string
19695
+ q:
19696
+ type: string
19697
+ typeahead:
19698
+ type: boolean
19699
+ email:
19700
+ type: string
19701
+ phone:
19702
+ type: string
19703
+ id:
19704
+ type: string
19705
+ userId:
19706
+ type: string
19707
+ tags:
19708
+ type: array
19709
+ items:
19710
+ type: object
19711
+ additionalProperties: true
19712
+ tagsAll:
19713
+ type: array
19714
+ items:
19715
+ type: object
19716
+ additionalProperties: true
19717
+ source:
19718
+ type: string
19719
+ locale:
19720
+ type: string
19721
+ createdFrom:
19722
+ type: string
19723
+ createdTo:
19724
+ type: string
19725
+ externalIdKey:
19726
+ type: string
19727
+ externalIdValue:
19728
+ type: string
19729
+ customFieldKey:
19730
+ type: string
19731
+ customFieldValue:
19732
+ type: string
19733
+ limit:
19734
+ type: number
19735
+ offset:
19736
+ type: number
19737
+ required:
19738
+ - collectionId
19739
+ ContactSearchResponse:
19740
+ type: object
19741
+ properties:
19742
+ items:
19743
+ type: array
19744
+ items:
19745
+ $ref: "#/components/schemas/Contact"
19746
+ limit:
19747
+ type: number
19748
+ offset:
19749
+ type: number
19750
+ required:
19751
+ - items
19752
+ - limit
19753
+ - offset
19583
19754
  PublicContactUpsertResponse:
19584
19755
  type: object
19585
19756
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.14.10",
3
+ "version": "1.14.12",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",