@villagehq/sdk 0.2.0-beta.1 → 0.2.0-beta.2
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/AGENTS.md +37 -26
- package/README.md +45 -8
- package/dist/core/paths-stores.d.ts +3 -18
- package/dist/core/paths-stores.js +11 -2
- package/dist/core/village.d.ts +1 -25
- package/dist/core/village.js +22 -1
- package/dist/elements/elements.js +8 -0
- package/dist/react/react.d.ts +8 -1
- package/dist/react/react.js +16 -1
- package/dist/server/server.d.ts +1 -25
- package/package.json +2 -1
package/AGENTS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Put warm introductions in your product: who your users know at a company, and who can introduce them. This page is the whole integration, written for AI coding agents and the developers working with them. Follow it top to bottom.
|
|
4
4
|
|
|
5
|
-
- Package: `@villagehq/sdk` (SDK version `0.2.0-beta.
|
|
5
|
+
- Package: `@villagehq/sdk` (SDK version `0.2.0-beta.2`).
|
|
6
6
|
- Published at https://docs.village.ai/build (raw: https://docs.village.ai/build.md). The copy in `node_modules/@villagehq/sdk/AGENTS.md` matches your installed version.
|
|
7
7
|
|
|
8
8
|
## 1. Quick start
|
|
@@ -10,7 +10,7 @@ Put warm introductions in your product: who your users know at a company, and wh
|
|
|
10
10
|
Build and verify locally first. No Village account, human approval, card, or real network is needed until Going live.
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install @villagehq/sdk@0.2.0-beta.
|
|
13
|
+
npm install @villagehq/sdk@0.2.0-beta.2
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Use the demo keys below and follow the chosen mode's code example. If billing is not specified yet, start with the payer `user` demo while asking who pays; local verification can continue without a human response. Render `<village-connect>` and `<village-facepile company="stripe.com">`, click Connect, and verify the synthetic result.
|
|
@@ -60,36 +60,46 @@ If the app pays, use **payer `app`**. If each user pays through their Village wo
|
|
|
60
60
|
|
|
61
61
|
```ts
|
|
62
62
|
// lib/village.ts
|
|
63
|
-
'use client';
|
|
64
|
-
|
|
65
63
|
import { createVillage } from '@villagehq/sdk';
|
|
66
|
-
import { defineVillageElements } from '@villagehq/sdk/elements';
|
|
67
64
|
|
|
68
65
|
export const village = createVillage({
|
|
69
66
|
publishableKey: process.env.NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY ?? '',
|
|
70
67
|
});
|
|
71
|
-
|
|
72
|
-
defineVillageElements(village); // does nothing on the server
|
|
73
68
|
```
|
|
74
69
|
|
|
75
70
|
```tsx
|
|
76
71
|
// components/warm-intros.tsx
|
|
77
72
|
'use client';
|
|
78
73
|
|
|
79
|
-
import type {} from '@villagehq/sdk/
|
|
80
|
-
import '
|
|
74
|
+
import type { VillagePathSelectDetail } from '@villagehq/sdk/elements';
|
|
75
|
+
import { useVillageElements } from '@villagehq/sdk/react'; // also the JSX types for <village-*>
|
|
76
|
+
import { useState } from 'react';
|
|
77
|
+
|
|
78
|
+
import { village } from '../lib/village';
|
|
79
|
+
|
|
80
|
+
function describeIntro({ person, introducer }: VillagePathSelectDetail) {
|
|
81
|
+
if (!introducer) return `You know ${person.name} directly.`;
|
|
82
|
+
return `Ask ${introducer.name} to introduce you to ${person.name}.`;
|
|
83
|
+
}
|
|
81
84
|
|
|
82
85
|
export function WarmIntros({ company }: { company: string }) {
|
|
86
|
+
const canRenderElements = useVillageElements(village); // registers them after hydration
|
|
87
|
+
const [selectedPath, setSelectedPath] = useState<VillagePathSelectDetail | null>(null);
|
|
88
|
+
if (!canRenderElements) return null;
|
|
89
|
+
|
|
83
90
|
return (
|
|
84
91
|
<section>
|
|
85
92
|
<village-connect />
|
|
86
93
|
<village-facepile company={company} />
|
|
87
|
-
<village-paths company={company} onvillage-path-select={(event) =>
|
|
94
|
+
<village-paths company={company} onvillage-path-select={(event) => setSelectedPath(event.detail)} />
|
|
95
|
+
{selectedPath && <p>{describeIntro(selectedPath)}</p>}
|
|
88
96
|
</section>
|
|
89
97
|
);
|
|
90
98
|
}
|
|
91
99
|
```
|
|
92
100
|
|
|
101
|
+
Render `<WarmIntros company="stripe.com" />` from any page. Paths are relative to your project root, or `src/` if you use one. Render the `<village-*>` elements only once `useVillageElements` returns `true`, never from server HTML: React doesn't attach `on*` listeners to custom elements it hydrates, and elements registered before hydration cause a hydration mismatch warning.
|
|
102
|
+
|
|
93
103
|
Import `village` from `lib/village` wherever you need `village.connect()`, `village.signOut()` or a store. That's the whole integration: no server code. Sessions live in `localStorage` (`village.session.<publishableKey>`), slide for 30 days, and renew on their own. `village.signOut()` disconnects the app; call it when your user signs out.
|
|
94
104
|
|
|
95
105
|
### Payer `app`, Next.js App Router
|
|
@@ -114,11 +124,11 @@ export async function POST(request: Request) {
|
|
|
114
124
|
The browser asks your route for it. Create the client per signed-in user:
|
|
115
125
|
|
|
116
126
|
```tsx
|
|
127
|
+
// components/warm-intros.tsx
|
|
117
128
|
'use client';
|
|
118
129
|
|
|
119
130
|
import { createVillage } from '@villagehq/sdk';
|
|
120
|
-
import {
|
|
121
|
-
import type {} from '@villagehq/sdk/react';
|
|
131
|
+
import { useVillageElements } from '@villagehq/sdk/react';
|
|
122
132
|
import { useState } from 'react';
|
|
123
133
|
|
|
124
134
|
async function readVillageToken() {
|
|
@@ -129,17 +139,17 @@ async function readVillageToken() {
|
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
function createUserVillage() {
|
|
132
|
-
|
|
142
|
+
return createVillage({
|
|
133
143
|
publishableKey: process.env.NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY ?? '',
|
|
134
144
|
token: readVillageToken,
|
|
135
145
|
});
|
|
136
|
-
defineVillageElements(village);
|
|
137
|
-
return village;
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
// Render with key={user.id}, so another user gets another client.
|
|
141
149
|
export function WarmIntros({ company }: { company: string }) {
|
|
142
|
-
useState(createUserVillage); // one client for this user, created once
|
|
150
|
+
const [village] = useState(createUserVillage); // one client for this user, created once
|
|
151
|
+
const canRenderElements = useVillageElements(village);
|
|
152
|
+
if (!canRenderElements) return null;
|
|
143
153
|
return (
|
|
144
154
|
<>
|
|
145
155
|
<village-connect />
|
|
@@ -155,8 +165,8 @@ The SDK calls `token` once, then refreshes it and retries once after a 401 mappe
|
|
|
155
165
|
|
|
156
166
|
```html
|
|
157
167
|
<script type="module">
|
|
158
|
-
import { createVillage } from 'https://esm.sh/@villagehq/sdk@0.2.0-beta.
|
|
159
|
-
import { defineVillageElements } from 'https://esm.sh/@villagehq/sdk@0.2.0-beta.
|
|
168
|
+
import { createVillage } from 'https://esm.sh/@villagehq/sdk@0.2.0-beta.2';
|
|
169
|
+
import { defineVillageElements } from 'https://esm.sh/@villagehq/sdk@0.2.0-beta.2/elements';
|
|
160
170
|
|
|
161
171
|
const village = createVillage({ publishableKey: 'pk_test_village_demo_user' }); // a publishable key is public
|
|
162
172
|
defineVillageElements(village);
|
|
@@ -192,17 +202,18 @@ Run the actual page on localhost with the public demo keys before you say you're
|
|
|
192
202
|
- [ ] Clicking it connects:
|
|
193
203
|
- payer `user`: the synthetic user connects without a Village login or account;
|
|
194
204
|
- payer `app`: with test keys, no modal opens: the test user connects at once. (On your server, `villageApp.test.connectUser({ id, email })` does the same; the page notices on its next `village.user.get()` or reload.)
|
|
195
|
-
- [ ] `<village-connect>` goes `syncing`, then `ready` within about 10 s.
|
|
205
|
+
- [ ] `<village-connect>` goes `syncing`, then `ready` within about 10 s. The elements wait without asking for paths meanwhile; if your own REST call gets `409 network_not_synced` during this sync, it is recoverable: call again once `GET /v2/user/status` reads `ready`.
|
|
196
206
|
- [ ] No Village account, human approval, card, or real network sync was needed for this demo.
|
|
197
207
|
- [ ] `<village-facepile company="stripe.com">` ends in `found` or `none`. Try 5 domains: the test network has paths for about 70% of them.
|
|
198
208
|
- [ ] Clicking an item in `<village-paths>` fires `village-path-select` with a `person`.
|
|
199
209
|
- [ ] Payer `user`: after `village.signOut()`, the elements go back to `signed_out`.
|
|
200
|
-
- [ ] The browser console has no CSP errors (see the reference) and no `VillageError`.
|
|
210
|
+
- [ ] The browser console has no hydration warning, no CSP errors (see the reference) and no `VillageError`.
|
|
201
211
|
- [ ] `VILLAGE_SECRET_KEY` appears only in server code, and `.env.local` is in `.gitignore`.
|
|
202
212
|
|
|
203
213
|
A script check for payer `app`:
|
|
204
214
|
|
|
205
215
|
```ts
|
|
216
|
+
// scripts/check-village.ts
|
|
206
217
|
import { createVillageApp } from '@villagehq/sdk/server';
|
|
207
218
|
|
|
208
219
|
const villageApp = createVillageApp({ secretKey: process.env.VILLAGE_SECRET_KEY ?? '' });
|
|
@@ -222,7 +233,7 @@ Wrapped SDK calls throw `VillageError` with `code`, `message`, `status`, `reques
|
|
|
222
233
|
| <a id="errors-signed_out"></a>`signed_out` | Payer `user`: no session in this browser | Render a button that calls `village.connect()` |
|
|
223
234
|
| <a id="errors-app_access_revoked"></a>`app_access_revoked` | The user removed your app from Village | Same as `signed_out`: the SDK already cleared the session |
|
|
224
235
|
| <a id="errors-not_connected"></a>`not_connected` | Payer `app`: the user hasn't linked Village | Call `village.connect()` from a click |
|
|
225
|
-
| <a id="errors-not_synced"></a>`not_synced` |
|
|
236
|
+
| <a id="errors-not_synced"></a>`not_synced` | HTTP 409 `network_not_synced`: the first network sync hasn't finished | Recoverable. Show progress; stores and elements load on their own once the network is `ready` |
|
|
226
237
|
| <a id="errors-out_of_credits"></a>`out_of_credits` | Out of credits. `error.payer` says whose when provided | `user`: link `error.topupUrl` if present. `app`: top up in the dashboard; payer can be null |
|
|
227
238
|
| <a id="errors-unauthorized"></a>`unauthorized` | Invalid key or token, or the app is inactive (live keys need a card for payer `app`) | `user`: connect again. `app`: check keys, token route and live billing; callback tokens retry once |
|
|
228
239
|
| <a id="errors-user_required"></a>`user_required` | A payer `user` app called Village without a signed-in user | Call `village.connect()` first; don't mint tokens for payer `user` apps |
|
|
@@ -245,7 +256,7 @@ Wrapped SDK calls throw `VillageError` with `code`, `message`, `status`, `reques
|
|
|
245
256
|
After the local demo passes, create your own Village app:
|
|
246
257
|
|
|
247
258
|
```bash
|
|
248
|
-
npx @villagehq/sdk@0.2.0-beta.
|
|
259
|
+
npx @villagehq/sdk@0.2.0-beta.2 init --name "Acme" --payer user # or --payer app
|
|
249
260
|
```
|
|
250
261
|
|
|
251
262
|
`init` defaults to `http://localhost:3000`. If your local app uses another port, pass its actual origin, for example `--origin http://localhost:3010`.
|
|
@@ -276,7 +287,7 @@ CLI flags: `--name`, `--origin`, `--payer app|user`, `--yes` (`-y`), `--help` (`
|
|
|
276
287
|
|
|
277
288
|
### Elements
|
|
278
289
|
|
|
279
|
-
Register
|
|
290
|
+
Register with `defineVillageElements(village)` (from `@villagehq/sdk/elements`). In React, use `useVillageElements(village)` instead and render the elements once it returns `true`, as in the Next.js code above. Registering another client switches every element on the page to it.
|
|
280
291
|
|
|
281
292
|
| Element | Attributes | Shows |
|
|
282
293
|
| -------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
@@ -329,7 +340,7 @@ Events (bubbling, composed `CustomEvent`s):
|
|
|
329
340
|
| `village-connected` | `{ user }`, after `connect()` leaves the user connected (not when they close the popup or modal without connecting) |
|
|
330
341
|
| `village-error` | `{ error }`, when `connect()` fails, e.g. `popup_blocked` |
|
|
331
342
|
|
|
332
|
-
**React 19 + Tailwind.** `import type {} from '@villagehq/sdk/react'` adds the JSX types. Events are `on` + the event name
|
|
343
|
+
**React 19 + Tailwind.** Importing from `@villagehq/sdk/react` (or `import type {} from '@villagehq/sdk/react'`) adds the JSX types. Events are `on` + the event name; they work on elements rendered after `useVillageElements` returns `true`:
|
|
333
344
|
|
|
334
345
|
```tsx
|
|
335
346
|
<village-facepile
|
|
@@ -378,7 +389,7 @@ What the stores do for you:
|
|
|
378
389
|
- **Shared**: the same company returns the same store, so 50 badges make one fetch.
|
|
379
390
|
- **Batched**: every `check` asked for in the same tick goes out as one request (100 companies each).
|
|
380
391
|
- **Credit-safe**: a store fetches on its first subscriber and keeps its result after the last one leaves. It never retries on its own; `refresh()` does.
|
|
381
|
-
- **Live**: a `not_synced`
|
|
392
|
+
- **Live**: while the first sync runs, a store waits as `not_synced` without asking Village for paths, then fetches once the network turns ready; every store resets on sign-out.
|
|
382
393
|
|
|
383
394
|
React:
|
|
384
395
|
|
|
@@ -393,7 +404,7 @@ function IntroBadge({ company }: { company: string }) {
|
|
|
393
404
|
}
|
|
394
405
|
```
|
|
395
406
|
|
|
396
|
-
Also `useVillageUser(village)` → `{ user, status, isLoading, error, refresh }`, `useVillageConnect(village)` → `{ connect, isConnecting }`, and `<VillageGate village fallback>`.
|
|
407
|
+
Also `useVillageElements(village)` → `true` once the elements can render (see section 4), `useVillageUser(village)` → `{ user, status, isLoading, error, refresh }`, `useVillageConnect(village)` → `{ connect, isConnecting }`, and `<VillageGate village fallback>`.
|
|
397
408
|
|
|
398
409
|
Vue:
|
|
399
410
|
|
package/README.md
CHANGED
|
@@ -5,17 +5,54 @@ Start at https://docs.village.ai/build.
|
|
|
5
5
|
Put Village warm introductions in your product: who your users know at a company, and who can introduce them. The complete guide also ships as [`AGENTS.md`](./AGENTS.md), matching your installed SDK version.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @villagehq/sdk@0.2.0-beta.
|
|
8
|
+
npm install @villagehq/sdk@0.2.0-beta.2
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Demo in two minutes (Next.js App Router)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
Add the public demo key to `.env.local`. It works on `localhost` against a synthetic network, with no account:
|
|
14
|
+
|
|
15
|
+
```dotenv
|
|
16
|
+
NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY=pk_test_village_demo_user
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// components/warm-intros.tsx
|
|
21
|
+
'use client';
|
|
22
|
+
|
|
23
|
+
import { createVillage } from '@villagehq/sdk';
|
|
24
|
+
import { useVillageElements } from '@villagehq/sdk/react';
|
|
25
|
+
|
|
26
|
+
const village = createVillage({
|
|
27
|
+
publishableKey: process.env.NEXT_PUBLIC_VILLAGE_PUBLISHABLE_KEY ?? '',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function WarmIntros({ company = 'stripe.com' }: { company?: string }) {
|
|
31
|
+
const canRenderElements = useVillageElements(village); // registers the elements after hydration
|
|
32
|
+
if (!canRenderElements) return null;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<section>
|
|
36
|
+
<village-connect />
|
|
37
|
+
<village-facepile company={company} />
|
|
38
|
+
<village-paths company={company} />
|
|
39
|
+
</section>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Render `<WarmIntros />` in `app/page.tsx`, restart `npm run dev` and open the page. You should see a **Connect Village** button. Click it: no login opens, it shows "Syncing your network…" for about 5 seconds, then "Village connected", a facepile with a strength label, and a list of people who can introduce you. The people are synthetic. About 30% of domains have no paths and show "No warm intros yet". The console stays free of errors.
|
|
45
|
+
|
|
46
|
+
There is no `Village.init`, `Village.findPaths` or default export, and `@village-ai/sdk` is another package: use `createVillage`, `village.paths.*` and the `<village-*>` elements.
|
|
47
|
+
|
|
48
|
+
Start locally with the public synthetic demo keys from the guide. No Village account, human approval, card, or real network is needed. Follow the chosen payer mode; if it is unclear, ask who pays asynchronously and verify the payer `user` demo while waiting. Run `npx @villagehq/sdk@0.2.0-beta.2 init` when you reach Going live.
|
|
49
|
+
|
|
50
|
+
| Import | What |
|
|
51
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `@villagehq/sdk` | `createVillage`: user status, `connect()`, stores (`village.paths.*`), search, paths, enrichment, the typed `api` |
|
|
53
|
+
| `@villagehq/sdk/elements` | `<village-connect>`, `<village-facepile>`, `<village-paths>` web components |
|
|
54
|
+
| `@villagehq/sdk/react` | `useVillageElements`, `useVillage(store)`, `useVillageUser`, `useVillageConnect`, `<VillageGate>`, JSX types for the elements |
|
|
55
|
+
| `@villagehq/sdk/server` | `createVillageApp({ secretKey })`: tokens, acting as a user, test users |
|
|
19
56
|
|
|
20
57
|
## Developing
|
|
21
58
|
|
|
@@ -55,29 +55,14 @@ type PathsStoresOptions<PersonPaths, CompanyPathsPage> = {
|
|
|
55
55
|
company: (page: CompanyPathsPage) => boolean;
|
|
56
56
|
};
|
|
57
57
|
watchUser: () => () => void;
|
|
58
|
+
isFirstSyncRunning: () => Promise<boolean>;
|
|
58
59
|
};
|
|
59
60
|
export type CompanyPathsStoreOptions = {
|
|
60
61
|
degrees?: Array<1 | 2 | 3>;
|
|
61
62
|
limit?: number;
|
|
62
63
|
};
|
|
63
|
-
export declare function createPathsStores<PersonPaths, CompanyPathsPage>({ payer, checkCompany, personPaths, companyPaths, hasPaths, watchUser, }: PathsStoresOptions<PersonPaths, CompanyPathsPage>): {
|
|
64
|
-
check: (company: string) => import("./store.js").VillageStore<
|
|
65
|
-
kind: "signed_out";
|
|
66
|
-
} | {
|
|
67
|
-
kind: "not_connected";
|
|
68
|
-
} | {
|
|
69
|
-
kind: "not_synced";
|
|
70
|
-
} | {
|
|
71
|
-
kind: "out_of_credits";
|
|
72
|
-
payer: Payer;
|
|
73
|
-
topupUrl: string | null;
|
|
74
|
-
} | LoadingState | NoneState | ErrorState | {
|
|
75
|
-
kind: "found";
|
|
76
|
-
people: FacepilePerson[];
|
|
77
|
-
more: number;
|
|
78
|
-
strength: number | null;
|
|
79
|
-
strength_label: string | null;
|
|
80
|
-
}>;
|
|
64
|
+
export declare function createPathsStores<PersonPaths, CompanyPathsPage>({ payer, checkCompany, personPaths, companyPaths, hasPaths, watchUser, isFirstSyncRunning, }: PathsStoresOptions<PersonPaths, CompanyPathsPage>): {
|
|
65
|
+
check: (company: string) => import("./store.js").VillageStore<PathsCheckState>;
|
|
81
66
|
person: (personInput: string) => import("./store.js").VillageStore<PathsState<PersonPaths>>;
|
|
82
67
|
company: (companyInput: string, { degrees, limit }?: CompanyPathsStoreOptions) => import("./store.js").VillageStore<PathsState<CompanyPathsPage>>;
|
|
83
68
|
reloadAll: () => void;
|
|
@@ -4,6 +4,7 @@ import { createStore } from './store.js';
|
|
|
4
4
|
const BULK_CHECK_LIMIT = 100;
|
|
5
5
|
const LOADING = { kind: 'loading' };
|
|
6
6
|
const NONE = { kind: 'none' };
|
|
7
|
+
const NOT_SYNCED = { kind: 'not_synced' };
|
|
7
8
|
const BLOCKED_STATE_BY_CODE = {
|
|
8
9
|
signed_out: { kind: 'signed_out' },
|
|
9
10
|
app_access_revoked: { kind: 'signed_out' },
|
|
@@ -61,7 +62,7 @@ function toCheckState(check) {
|
|
|
61
62
|
function waitsForNetwork(state) {
|
|
62
63
|
return state.kind === 'not_synced';
|
|
63
64
|
}
|
|
64
|
-
export function createPathsStores({ payer, checkCompany, personPaths, companyPaths, hasPaths, watchUser, }) {
|
|
65
|
+
export function createPathsStores({ payer, checkCompany, personPaths, companyPaths, hasPaths, watchUser, isFirstSyncRunning, }) {
|
|
65
66
|
const checks = new Map();
|
|
66
67
|
const people = new Map();
|
|
67
68
|
const companies = new Map();
|
|
@@ -84,11 +85,17 @@ export function createPathsStores({ payer, checkCompany, personPaths, companyPat
|
|
|
84
85
|
}
|
|
85
86
|
function check(company) {
|
|
86
87
|
const identifier = toCompanyIdentifier(company);
|
|
87
|
-
return cached(checks, JSON.stringify(identifier), LOADING, () =>
|
|
88
|
+
return cached(checks, JSON.stringify(identifier), LOADING, async () => {
|
|
89
|
+
if (await isFirstSyncRunning())
|
|
90
|
+
return NOT_SYNCED;
|
|
91
|
+
return checkCompany(identifier).then(toCheckState, toFailureState);
|
|
92
|
+
});
|
|
88
93
|
}
|
|
89
94
|
function person(personInput) {
|
|
90
95
|
const identifier = toPersonIdentifier(personInput);
|
|
91
96
|
return cached(people, JSON.stringify(identifier), LOADING, async () => {
|
|
97
|
+
if (await isFirstSyncRunning())
|
|
98
|
+
return NOT_SYNCED;
|
|
92
99
|
try {
|
|
93
100
|
const paths = await personPaths(personInput);
|
|
94
101
|
if (paths === null || !hasPaths.person(paths))
|
|
@@ -104,6 +111,8 @@ export function createPathsStores({ payer, checkCompany, personPaths, companyPat
|
|
|
104
111
|
const identifier = toCompanyIdentifier(companyInput);
|
|
105
112
|
const key = JSON.stringify([identifier, degrees ?? null, limit ?? null]);
|
|
106
113
|
return cached(companies, key, LOADING, async () => {
|
|
114
|
+
if (await isFirstSyncRunning())
|
|
115
|
+
return NOT_SYNCED;
|
|
107
116
|
try {
|
|
108
117
|
const page = await companyPaths(companyInput, { ...(degrees ? { degrees } : {}), ...(limit ? { limit } : {}) });
|
|
109
118
|
if (page === null || !hasPaths.company(page))
|
package/dist/core/village.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { components, paths } from '../generated/api.js';
|
|
2
|
-
import type { Payer } from './api-types.js';
|
|
3
2
|
import { isVillageError, VillageError } from './errors.js';
|
|
4
3
|
import { type Page } from './pages.js';
|
|
5
4
|
import { type SessionStorage } from './session.js';
|
|
@@ -78,30 +77,7 @@ export declare function createVillage(options: VillageOptions): {
|
|
|
78
77
|
connect: () => Promise<VillageUser>;
|
|
79
78
|
signOut: () => Promise<void>;
|
|
80
79
|
paths: {
|
|
81
|
-
check: (company: string) => import("./store.js").VillageStore<
|
|
82
|
-
kind: "signed_out";
|
|
83
|
-
} | {
|
|
84
|
-
kind: "not_connected";
|
|
85
|
-
} | {
|
|
86
|
-
kind: "not_synced";
|
|
87
|
-
} | {
|
|
88
|
-
kind: "out_of_credits";
|
|
89
|
-
payer: Payer;
|
|
90
|
-
topupUrl: string | null;
|
|
91
|
-
} | {
|
|
92
|
-
kind: "loading";
|
|
93
|
-
} | {
|
|
94
|
-
kind: "none";
|
|
95
|
-
} | {
|
|
96
|
-
kind: "error";
|
|
97
|
-
error: VillageError;
|
|
98
|
-
} | {
|
|
99
|
-
kind: "found";
|
|
100
|
-
people: import("./api-types.js").FacepilePerson[];
|
|
101
|
-
more: number;
|
|
102
|
-
strength: number | null;
|
|
103
|
-
strength_label: string | null;
|
|
104
|
-
}>;
|
|
80
|
+
check: (company: string) => import("./store.js").VillageStore<import("./paths-stores.js").PathsCheckState>;
|
|
105
81
|
person: (personInput: string) => import("./store.js").VillageStore<import("./paths-stores.js").PathsState<{
|
|
106
82
|
person: components["schemas"]["PersonInfo"];
|
|
107
83
|
summary: components["schemas"]["PathSummary"];
|
package/dist/core/village.js
CHANGED
|
@@ -13,6 +13,7 @@ export { linkedinSlug, linkedinUrl } from './identifiers.js';
|
|
|
13
13
|
export { openSyncModal } from './sync-modal.js';
|
|
14
14
|
export { isNetworkReady } from './user.js';
|
|
15
15
|
const DEFAULT_APP_URL = 'https://village.ai';
|
|
16
|
+
const DEMO_KEYS_URL = 'https://docs.village.ai/build#2-public-demo-keys';
|
|
16
17
|
const SEARCH_TIMEOUT_MS = 120_000;
|
|
17
18
|
const PATHS_TIMEOUT_MS = 60_000;
|
|
18
19
|
const TEST_MODE = 'test';
|
|
@@ -43,7 +44,7 @@ function requirePublishableKey(publishableKey) {
|
|
|
43
44
|
return publishableKey;
|
|
44
45
|
throw new VillageError({
|
|
45
46
|
code: 'bad_request',
|
|
46
|
-
message:
|
|
47
|
+
message: `Pass createVillage({ publishableKey }) when users pay, or { token } when your app pays. To build locally, use the public demo key pk_test_village_demo_user (no account needed): ${DEMO_KEYS_URL}`,
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
function signedOutError() {
|
|
@@ -149,6 +150,25 @@ export function createVillage(options) {
|
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
const { store: user, replace: replaceUser } = createUserStore(readUser, handleUserChange);
|
|
153
|
+
let sharedUserRead = null;
|
|
154
|
+
// Stores that load together share one status read.
|
|
155
|
+
function readUserOnce() {
|
|
156
|
+
sharedUserRead ??= user
|
|
157
|
+
.get()
|
|
158
|
+
.catch(() => null)
|
|
159
|
+
.finally(() => {
|
|
160
|
+
sharedUserRead = null;
|
|
161
|
+
});
|
|
162
|
+
return sharedUserRead;
|
|
163
|
+
}
|
|
164
|
+
async function isFirstSyncRunning() {
|
|
165
|
+
if (isNetworkReady(user.snapshot().user))
|
|
166
|
+
return false;
|
|
167
|
+
await readUserOnce();
|
|
168
|
+
// The snapshot, not this read: a newer read may already have found the network ready.
|
|
169
|
+
const latest = user.snapshot().user;
|
|
170
|
+
return latest?.status === 'syncing' && latest.network_updated_at === null;
|
|
171
|
+
}
|
|
152
172
|
function endSession() {
|
|
153
173
|
replaceUser(SIGNED_OUT_USER);
|
|
154
174
|
pathStores.reloadAll();
|
|
@@ -268,6 +288,7 @@ export function createVillage(options) {
|
|
|
268
288
|
},
|
|
269
289
|
// A fresh listener each time: the user store keeps listeners in a Set.
|
|
270
290
|
watchUser: () => user.subscribe(() => undefined),
|
|
291
|
+
isFirstSyncRunning,
|
|
271
292
|
});
|
|
272
293
|
async function joinGroup(inviteLink, options) {
|
|
273
294
|
try {
|
|
@@ -132,9 +132,11 @@ function createElementClasses(requireVillage) {
|
|
|
132
132
|
connectedCallback() {
|
|
133
133
|
this.unbind();
|
|
134
134
|
this.stopWatching = this.watch(requireVillage());
|
|
135
|
+
connectedElements.add(this);
|
|
135
136
|
}
|
|
136
137
|
disconnectedCallback() {
|
|
137
138
|
this.unbind();
|
|
139
|
+
connectedElements.delete(this);
|
|
138
140
|
}
|
|
139
141
|
attributeChangedCallback() {
|
|
140
142
|
if (this.stopWatching)
|
|
@@ -286,13 +288,16 @@ function createElementClasses(requireVillage) {
|
|
|
286
288
|
};
|
|
287
289
|
}
|
|
288
290
|
let activeVillage = null;
|
|
291
|
+
const connectedElements = new Set();
|
|
289
292
|
function requireVillage() {
|
|
290
293
|
if (activeVillage)
|
|
291
294
|
return activeVillage;
|
|
292
295
|
throw new Error('Call defineVillageElements(village) before using Village elements.');
|
|
293
296
|
}
|
|
294
297
|
// Safe to import on a server: nothing touches the DOM until this runs in a browser.
|
|
298
|
+
// Call it again with another client (another signed-in user) and every element on the page switches to it.
|
|
295
299
|
export function defineVillageElements(village) {
|
|
300
|
+
const isNewClient = activeVillage !== null && activeVillage !== village;
|
|
296
301
|
activeVillage = village;
|
|
297
302
|
if (typeof customElements === 'undefined')
|
|
298
303
|
return;
|
|
@@ -300,4 +305,7 @@ export function defineVillageElements(village) {
|
|
|
300
305
|
if (!customElements.get(tag))
|
|
301
306
|
customElements.define(tag, elementClass);
|
|
302
307
|
}
|
|
308
|
+
if (isNewClient)
|
|
309
|
+
for (const element of connectedElements)
|
|
310
|
+
element.connectedCallback();
|
|
303
311
|
}
|
package/dist/react/react.d.ts
CHANGED
|
@@ -2,7 +2,14 @@ import { type DetailedHTMLProps, type HTMLAttributes, type ReactNode } from 'rea
|
|
|
2
2
|
import type { VillageStore } from '../core/store.js';
|
|
3
3
|
import { type VillageUser } from '../core/user.js';
|
|
4
4
|
import type { Village } from '../core/village.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type VillageConnectedDetail, type VillageErrorDetail, type VillagePathSelectDetail } from '../elements/elements.js';
|
|
6
|
+
/**
|
|
7
|
+
* Registers the Village elements for this client after hydration and returns true once they can
|
|
8
|
+
* render. Render `<village-*>` only when it does: React doesn't attach `on*` listeners to custom
|
|
9
|
+
* elements it hydrates from server HTML, an element defined before hydration causes a mismatch
|
|
10
|
+
* warning, and an element mounted before its client is registered would show the previous client's data.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useVillageElements(village: Village): boolean;
|
|
6
13
|
export declare function useVillage<State>(store: VillageStore<State>): State;
|
|
7
14
|
export declare function useVillageUser(village: Village): {
|
|
8
15
|
user: VillageUser | null;
|
package/dist/react/react.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useCallback, useState, useSyncExternalStore, } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useState, useSyncExternalStore, } from 'react';
|
|
3
3
|
import { isNetworkReady } from '../core/user.js';
|
|
4
|
+
import { defineVillageElements, } from '../elements/elements.js';
|
|
4
5
|
function ignoreFailure() {
|
|
5
6
|
return undefined;
|
|
6
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Registers the Village elements for this client after hydration and returns true once they can
|
|
10
|
+
* render. Render `<village-*>` only when it does: React doesn't attach `on*` listeners to custom
|
|
11
|
+
* elements it hydrates from server HTML, an element defined before hydration causes a mismatch
|
|
12
|
+
* warning, and an element mounted before its client is registered would show the previous client's data.
|
|
13
|
+
*/
|
|
14
|
+
export function useVillageElements(village) {
|
|
15
|
+
const [registeredVillage, setRegisteredVillage] = useState(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
defineVillageElements(village);
|
|
18
|
+
setRegisteredVillage(village);
|
|
19
|
+
}, [village]);
|
|
20
|
+
return registeredVillage === village;
|
|
21
|
+
}
|
|
7
22
|
export function useVillage(store) {
|
|
8
23
|
const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
|
|
9
24
|
return useSyncExternalStore(subscribe, store.get, store.get);
|
package/dist/server/server.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { paths } from '../generated/api.js';
|
|
2
|
-
import { VillageError } from '../core/errors.js';
|
|
3
2
|
import { type CallOptions, type TransportOptions } from '../core/transport.js';
|
|
4
3
|
type AppUpdate = NonNullable<paths['/v2/app']['patch']['requestBody']>['content']['application/json'];
|
|
5
4
|
export type TokenGrant = 'full' | 'public';
|
|
@@ -23,30 +22,7 @@ export declare function createVillageApp({ secretKey, appUrl, ...transportOption
|
|
|
23
22
|
connect: () => Promise<import("../core/user.js").VillageUser>;
|
|
24
23
|
signOut: () => Promise<void>;
|
|
25
24
|
paths: {
|
|
26
|
-
check: (company: string) => import("../core/store.js").VillageStore<
|
|
27
|
-
kind: "signed_out";
|
|
28
|
-
} | {
|
|
29
|
-
kind: "not_connected";
|
|
30
|
-
} | {
|
|
31
|
-
kind: "not_synced";
|
|
32
|
-
} | {
|
|
33
|
-
kind: "out_of_credits";
|
|
34
|
-
payer: import("../core/api-types.js").Payer;
|
|
35
|
-
topupUrl: string | null;
|
|
36
|
-
} | {
|
|
37
|
-
kind: "loading";
|
|
38
|
-
} | {
|
|
39
|
-
kind: "none";
|
|
40
|
-
} | {
|
|
41
|
-
kind: "error";
|
|
42
|
-
error: VillageError;
|
|
43
|
-
} | {
|
|
44
|
-
kind: "found";
|
|
45
|
-
people: import("../core/api-types.js").FacepilePerson[];
|
|
46
|
-
more: number;
|
|
47
|
-
strength: number | null;
|
|
48
|
-
strength_label: string | null;
|
|
49
|
-
}>;
|
|
25
|
+
check: (company: string) => import("../core/store.js").VillageStore<import("../core/paths-stores.js").PathsCheckState>;
|
|
50
26
|
person: (personInput: string) => import("../core/store.js").VillageStore<import("../core/paths-stores.js").PathsState<{
|
|
51
27
|
person: import("../generated/api.js").components["schemas"]["PersonInfo"];
|
|
52
28
|
summary: import("../generated/api.js").components["schemas"]["PathSummary"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@villagehq/sdk",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.2",
|
|
4
4
|
"description": "Put Village warm intros in your app: intro paths, people and company search, drop-in web components, and a setup CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"village",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@testing-library/react": "^16.3.0",
|
|
67
67
|
"@types/react": "^19.1.0",
|
|
68
|
+
"@types/react-dom": "^19.3.0",
|
|
68
69
|
"jsdom": "^26.1.0",
|
|
69
70
|
"openapi-typescript": "^7.10.0",
|
|
70
71
|
"react": "^19.1.0",
|