@basictech/react 0.11.0-beta.1 → 0.11.0-beta.3

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/README.md CHANGED
@@ -26,6 +26,80 @@ npm install @basictech/react @basictech/schema
26
26
 
27
27
  React is a peer dependency and must already be installed by your application.
28
28
 
29
+ ## Beta auth update guide
30
+
31
+ After this auth update is published to the `beta` dist-tag, upgrade the packages you use together:
32
+
33
+ ```bash
34
+ npm install @basictech/react@beta @basictech/schema@beta
35
+ # If you import core directly, update that dependency too:
36
+ npm install @basictech/core@beta
37
+ ```
38
+
39
+ Keep the public packages on matching beta versions. No browser storage reset or persisted-profile
40
+ migration is required. Check your lockfile for the resolved version; this guide does not imply
41
+ that an older installed beta contains the update.
42
+
43
+ ### Update auth checks and editing controls
44
+
45
+ - Replace `useAuth().status === 'reauth_required'` with `status === 'expired'`.
46
+ Core client snapshots likewise use `authStatus: 'expired'`; only low-level `AuthSession`
47
+ retains the legacy compatibility state.
48
+ - Use `auth.canWrite` to disable mutation controls and `auth.readOnlyReason` for explanation.
49
+ It is account eligibility, not a guarantee of connectivity or permission to a particular source.
50
+ Catch imperative mutation errors too: auth can expire after the UI renders. Newly attempted
51
+ expired mutations reject with `BasicError.code === 'AUTH_EXPIRED'`.
52
+ - Keep cached data visible when expired. Do not treat `!auth.isSignedIn` as instructions to
53
+ sign out, remove the account, or clear local storage. Expiry retains identity, cached records,
54
+ and already queued edits, but blocks new writes and pauses sync sends.
55
+ - Offer `auth.signIn()` to reauthorize the same DID/issuer. For a different identity, add or switch
56
+ accounts first. Pending data must not be replayed into another account.
57
+
58
+ ```tsx
59
+ // Uses the schema-bound basic instance from the quick start below.
60
+ function AuthNotice() {
61
+ const auth = basic.useAuth()
62
+ if (auth.status !== 'expired') return null
63
+ return (
64
+ <aside role="status">
65
+ Session expired. Cached data is read-only; your pending edits are retained.
66
+ <button onClick={() => void auth.signIn()}>Sign in again</button>
67
+ </aside>
68
+ )
69
+ }
70
+ // In your editor: <button disabled={!auth.canWrite} ...>Save</button>
71
+ ```
72
+
73
+ ### Render per-account status
74
+
75
+ `useAccounts().accounts` now returns `BasicAccount[]`, including anonymous and expired accounts.
76
+ Each account adds `auth: { status, reason, checkedAt }` to the existing profile fields:
77
+
78
+ | Account status | Meaning |
79
+ | --- | --- |
80
+ | `anon` | Local anonymous workspace; editable without credentials |
81
+ | `checking` | Authorization has not been verified for this remembered account |
82
+ | `authenticated` | Current runtime authentication succeeded |
83
+ | `recovering` | Transient failure; ordinary offline queues remain writable |
84
+ | `expired` | Interactive reauthorization required; retain data read-only |
85
+ | `signed_out` | No usable active authorization |
86
+
87
+ `checkedAt` is the local evaluation time in milliseconds since epoch, or null—not a guarantee
88
+ from the server. Inactive accounts are not all refreshed: retained identity without credentials
89
+ is expired; otherwise an unverified remembered account is checking. The active anonymous account
90
+ has `auth.status: 'anon'`, while top-level `useAuth()` reports `status: 'signed_out'` and
91
+ `isAnonymous: true`. REST exposes one remembered account, not a multi-account registry.
92
+
93
+ Ordinary network loss is not expiry, and anonymous editing still works. `goOnline()` cannot
94
+ bypass expired read-only enforcement. Reauthorization resumes retained queued work; already
95
+ accepted server requests cannot be undone by client cancellation. REST has no offline cache.
96
+
97
+ Verified session revocation and automatic account/data cleanup are deferred to a later update.
98
+ Generic `invalid_grant` is not evidence that it is safe to erase local data. Browser storage
99
+ remains unchanged: localStorage holds profiles/refresh credentials, sessionStorage holds tab
100
+ selection/PKCE, access tokens live in memory, and Dexie wraps IndexedDB for replica data.
101
+ The default browser adapter writes no auth cookies. Read-only does not encrypt cached data.
102
+
29
103
  ## Quick start
30
104
 
31
105
  Define the schema and create `basic` once at module scope. The returned client, Provider, and hooks
@@ -151,8 +225,8 @@ client.
151
225
  | --- | --- | --- | --- |
152
226
  | `clientId` | `string` | Required | The application's client DID. Its Basic app metadata must register the redirect URI. |
153
227
  | `schema` | `S extends BasicSchema` | Required by `createBasic` | Pass the `defineSchema()` result used by this application. It is optional only in the lower-level `createBasicClient`. |
154
- | `identityOrigin` | `string` | `'https://basic.id'` | Use another Identity v1 origin for handle resolution and the shares management URL. |
155
- | `defaultPds` | `string` | `'https://pds.basic.id'` | Change the PDS discovered by no-argument `signIn()`. `signIn(handleOrDid)` discovers that identity's PDS instead. |
228
+ | `identityOrigin` | `string` | `'https://basic.id'` | Use another Basic ID web origin for the shares management URL. |
229
+ | `defaultPds` | `string` | `'https://pds.basic.id'` | Change the PDS used for handle resolution and no-argument `signIn()`. `signIn(handleOrDid)` discovers that identity's PDS after resolving handles here. |
156
230
  | `redirectUri` | `string` | `location.origin + location.pathname` | Set an explicit OAuth callback route. Outside a browser location, it is required. The URI must appear in the client metadata for `clientId`. |
157
231
  | `mode` | `'sync' \| 'rest'` | `'sync'` | Choose direct REST when offline replicas, anonymous mode, multi-account state, and `watch()` are not needed. |
158
232
  | `anonymous` | `boolean` | `true` in sync mode | Set `false` to require sign-in. REST mode always disables anonymous workspaces. |
@@ -170,6 +244,7 @@ client.
170
244
  | `currentUrl` | `() => string` | `location.href`, or `''` without `location` | Tell auth bootstrap where to read OAuth `code` and `state`. |
171
245
  | `replaceUrl` | `(url: string) => void` | `history.replaceState({}, '', url)` | Integrate removal of consumed OAuth query parameters with custom navigation. |
172
246
  | `createMessageChannel` | `(name: string) => AuthMessageChannel` | `BroadcastChannel` when available | Replace or disable cross-context auth notifications. The default token adapter also transfers access tokens transiently between tabs. |
247
+ | `reconcileTrigger` | `(listener: () => void) => () => void` | `focus`, `online`, and `visibilitychange` listeners | Subscribe the client to host lifecycle signals so a suspended or backgrounded application reconciles credentials with its owner and mounted Sync sessions on resume. Return a cleanup function; the client calls it on `stop()`. |
173
248
  | `ownerCredential` | `OwnerCredentialAdapter` | None | First-party Basic ID/dev tooling can inject owner credentials for `client.drive()`, account-wide `client.storageInfo()`, and `client.shares.leave()`. Normal app credentials should use existing repos and `shares.manageUrl()`. |
174
249
  | `uploadTransport` | `UploadTransportAdapter` | Browser XHR, then the core fetch fallback | Supply a multipart transport. XHR is used by default because fetch has no upload-progress events. |
175
250
  | `warn` | `(message: string) => void` | `console.warn` for schema checks | Route schema-drift warnings into application logging. |
@@ -192,6 +267,10 @@ The defaults are selected from browser capabilities at client creation:
192
267
  - **Auth coordination:** token rotation and sign-out notifications use `BroadcastChannel` when
193
268
  available. Access tokens can cross that channel transiently but are not written by the default
194
269
  token store.
270
+ - **Lifecycle recovery:** when a tab regains focus, comes back online, or becomes visible again,
271
+ the client reconciles fresh credentials with the owner and mounted Sync sessions. A browser that
272
+ suspended the page recovers without a reload, and transient refresh failures retry rather than
273
+ signing the profile out. Override `reconcileTrigger` to supply that signal in another host.
195
274
  - **Uploads:** browser multipart uploads use XHR for real `onProgress(loaded, total)` events. The
196
275
  fetch fallback reports only start and completion.
197
276
 
@@ -271,7 +350,9 @@ unknown table names and infer fields in reads, writes, and queries.
271
350
  | `isReady` | `boolean` | Auth bootstrap has left `bootstrapping` |
272
351
  | `isSignedIn` | `boolean` | Status is `authenticated` or `recovering` |
273
352
  | `isAnonymous` | `boolean` | The active sync profile is anonymous and not signed in |
274
- | `status` | `'bootstrapping' \| 'signed_out' \| 'authenticated' \| 'recovering' \| 'reauth_required'` | Current auth lifecycle |
353
+ | `status` | `'bootstrapping' \| 'signed_out' \| 'authenticated' \| 'recovering' \| 'expired'` | Current public auth lifecycle |
354
+ | `canWrite` | `boolean` | Active-account write eligibility; source permissions still apply |
355
+ | `readOnlyReason` | `string \| null` | For example `AUTH_EXPIRED` when reauthorization is needed |
275
356
  | `error` | `BasicError \| null` | Stable auth bootstrap/refresh error code when available |
276
357
  | `user` | `AuthUser \| null` | OIDC user info (`sub`, `pds_url`, and optional `email`, `name`, `picture`, `handle`) |
277
358
  | `did` | `string \| null` | Signed-in account DID |
@@ -288,6 +369,9 @@ export function AuthPanel() {
288
369
  const auth = basic.useAuth()
289
370
 
290
371
  if (!auth.isReady) return <p>Starting Basic…</p>
372
+ if (auth.status === 'expired') {
373
+ return <p>Session expired. Cached data is read-only. <button onClick={() => void auth.signIn()}>Sign in again</button></p>
374
+ }
291
375
  if (auth.isSignedIn) {
292
376
  return (
293
377
  <p>
@@ -317,8 +401,8 @@ replicas and returns to another local profile or a fresh anonymous one when enab
317
401
 
318
402
  `useAccounts()` returns `{ accounts, active, switchAccount, addAccount, removeAccount }`:
319
403
 
320
- - `accounts: BasicProfile[]` lists local profiles.
321
- - `active: BasicProfile | null` is this tab's selected profile.
404
+ - `accounts: BasicAccount[]` lists local profiles with per-account auth summaries.
405
+ - `active: BasicAccount | null` is this tab's selected profile and auth summary.
322
406
  - `switchAccount(id): Promise<void>` changes the active profile in this tab.
323
407
  - `addAccount(): Promise<BasicProfile>` creates and activates a new anonymous profile from which to
324
408
  start another sign-in.
@@ -745,6 +829,7 @@ Shares v2 is capability-gated and requires sign-in. `useOutgoingShares()` return
745
829
  get(id: string): Promise<OutgoingShareInfo>
746
830
  cancel(id: string): Promise<OutgoingShareInfo>
747
831
  revoke(id: string): Promise<OutgoingShareInfo>
832
+ getContactHandle(did: string): Promise<string | null>
748
833
  manageUrl(): string
749
834
  }
750
835
  ```
@@ -754,7 +839,7 @@ Shares v2 is capability-gated and requires sign-in. `useOutgoingShares()` return
754
839
  | Field | Type | Meaning |
755
840
  | --- | --- | --- |
756
841
  | `repo` | `'default' \| { repoId: string }` | Origin repo |
757
- | recipient | Exactly one of `recipientHandle: string` or `recipientDid: string` | A handle is trimmed, lowercased, and resolved through `identityOrigin`; a canonical DID skips handle resolution. |
842
+ | recipient | Exactly one of `recipientHandle: string` or `recipientDid: string` | A handle is trimmed, lowercased, and resolved through `defaultPds`; a canonical DID skips handle resolution. |
758
843
  | `role` | `'viewer' \| 'editor'` | Collaborator role |
759
844
  | `scope` | `Array<{ table: string } \| { table: string; recordIds: string[] }>` | Whole-table clauses or static record sets |
760
845
  | `acceptBy` | `string` | Optional acceptance deadline passed to Shares v2 |
@@ -807,6 +892,10 @@ export function OutgoingShares() {
807
892
  `deliveryState`, and created, accepted, and ended timestamps/reasons. Revocation prevents future
808
893
  access but cannot retract data a recipient already downloaded.
809
894
 
895
+ After a handle-based share is created successfully, the SDK stores a profile-local contact entry
896
+ containing `{ did, handle }` in browser local storage. Use `getContactHandle(recipientDid)` to show
897
+ the remembered handle; it returns `null` for DIDs that were shared directly or are not cached.
898
+
810
899
  `manageUrl()` returns the Basic ID `/shares` handoff. Invite acceptance and ordinary mount
811
900
  management belong there. `client.shares.leave()` is intentionally owner-only and throws
812
901
  `OWNER_CREDENTIAL_REQUIRED` without an explicit owner adapter.
@@ -1037,6 +1126,8 @@ Common application-facing codes include:
1037
1126
  | Code | Typical cause |
1038
1127
  | --- | --- |
1039
1128
  | `AUTHORIZATION_REQUIRED` | A signed-in token/session is required |
1129
+ | `AUTH_EXPIRED` | Session needs reauthorization; cached data and pending edits are retained |
1130
+ | `ACCOUNT_CHANGED` | The account changed while an operation was in flight; do not blindly retry into the new account |
1040
1131
  | `UNKNOWN_SOURCE` | The repo is not in the catalog or the mount has not been listed/opened |
1041
1132
  | `RECORD_NOT_FOUND`, `RECORD_EXISTS`, `RECORD_NOT_DELETED` | Record lifecycle precondition failed |
1042
1133
  | `INVALID_PATCH`, `INVALID_RECORD_ID`, `INVALID_IDEMPOTENCY_KEY` | Invalid local mutation input |
@@ -1093,8 +1184,8 @@ for the lower-level model.
1093
1184
  Client Components should import `@basictech/react` directly.
1094
1185
  - Migrating from 0.10 requires deliberate call-site changes; there is no compatibility layer. Read
1095
1186
  [Migrating to Basic SDK 0.11](https://github.com/basicdb/basic-server/blob/dev/docs/MIGRATING_TO_0.11.md).
1096
- - `0.11.0-beta.0` is a beta API. Check the installed version rather than assuming the repository
1097
- workspace has already been published.
1187
+ - 0.11 is a beta API, published to npm under the `beta` dist-tag. Check the installed version
1188
+ rather than assuming it matches the repository workspace.
1098
1189
 
1099
1190
  ## License
1100
1191
 
package/dist/index.d.mts CHANGED
@@ -1,14 +1,14 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ReactElement } from 'react';
3
- import { BasicSchema, BasicConfig, BasicClient, BasicProfile, AuthStatus, BasicError, AuthUser, BasicSyncStatus, BasicRejection, BasicConflict, SourceRef, SchemaStatus, BasicDb, Repo, FileRecord, MountFileInfo, StorageInfo, FileListQuery, MountInfo, MountHandle, OutgoingShareInfo, CreateOutgoingShareInput, MountsQuery, JsonObject, BasicRecord, Query, MountViewerFiles, OwnerFiles, TableNames, Collection, InferValue, KeyValueStorage, TokenStore, AuthMessageChannel, ReplicaStoreFactory, StoragePartition, ReplicaStore, UploadTransportAdapter } from '@basictech/core';
3
+ import { BasicSchema, BasicConfig, BasicClient, BasicAccount, BasicProfile, AuthStatus, BasicError, AuthUser, BasicSyncStatus, BasicRejection, BasicConflict, SourceRef, SchemaStatus, BasicDb, Repo, FileRecord, MountFileInfo, StorageInfo, FileListQuery, MountInfo, MountHandle, OutgoingShareInfo, CreateOutgoingShareInput, MountsQuery, JsonObject, BasicRecord, Query, MountViewerFiles, OwnerFiles, TableNames, Collection, InferValue, KeyValueStorage, TokenStore, AuthMessageChannel, ReplicaStoreFactory, StoragePartition, ReplicaStore, UploadTransportAdapter } from '@basictech/core';
4
4
 
5
5
  type BrowserBasicConfig<S extends BasicSchema = BasicSchema> = BasicConfig<S>;
6
6
  /** Create a core client with the complete browser adapter set installed. */
7
7
  declare function createBasicClient<S extends BasicSchema>(config: BrowserBasicConfig<S>): BasicClient<S>;
8
8
 
9
9
  interface UseAccountsResult {
10
- accounts: BasicProfile[];
11
- active: BasicProfile | null;
10
+ accounts: BasicAccount[];
11
+ active: BasicAccount | null;
12
12
  switchAccount(id: string): Promise<void>;
13
13
  addAccount(): Promise<BasicProfile>;
14
14
  removeAccount(id: string): Promise<void>;
@@ -19,6 +19,8 @@ interface UseAuthResult {
19
19
  isReady: boolean;
20
20
  isSignedIn: boolean;
21
21
  isAnonymous: boolean;
22
+ canWrite: boolean;
23
+ readOnlyReason: string | null;
22
24
  status: AuthStatus;
23
25
  error: BasicError | null;
24
26
  user: AuthUser | null;
@@ -87,6 +89,7 @@ interface UseOutgoingSharesResult {
87
89
  get(id: string): Promise<OutgoingShareInfo>;
88
90
  cancel(id: string): Promise<OutgoingShareInfo>;
89
91
  revoke(id: string): Promise<OutgoingShareInfo>;
92
+ getContactHandle(did: string): Promise<string | null>;
90
93
  manageUrl(): string;
91
94
  }
92
95
  declare function useOutgoingShares(): UseOutgoingSharesResult;
package/dist/index.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ReactElement } from 'react';
3
- import { BasicSchema, BasicConfig, BasicClient, BasicProfile, AuthStatus, BasicError, AuthUser, BasicSyncStatus, BasicRejection, BasicConflict, SourceRef, SchemaStatus, BasicDb, Repo, FileRecord, MountFileInfo, StorageInfo, FileListQuery, MountInfo, MountHandle, OutgoingShareInfo, CreateOutgoingShareInput, MountsQuery, JsonObject, BasicRecord, Query, MountViewerFiles, OwnerFiles, TableNames, Collection, InferValue, KeyValueStorage, TokenStore, AuthMessageChannel, ReplicaStoreFactory, StoragePartition, ReplicaStore, UploadTransportAdapter } from '@basictech/core';
3
+ import { BasicSchema, BasicConfig, BasicClient, BasicAccount, BasicProfile, AuthStatus, BasicError, AuthUser, BasicSyncStatus, BasicRejection, BasicConflict, SourceRef, SchemaStatus, BasicDb, Repo, FileRecord, MountFileInfo, StorageInfo, FileListQuery, MountInfo, MountHandle, OutgoingShareInfo, CreateOutgoingShareInput, MountsQuery, JsonObject, BasicRecord, Query, MountViewerFiles, OwnerFiles, TableNames, Collection, InferValue, KeyValueStorage, TokenStore, AuthMessageChannel, ReplicaStoreFactory, StoragePartition, ReplicaStore, UploadTransportAdapter } from '@basictech/core';
4
4
 
5
5
  type BrowserBasicConfig<S extends BasicSchema = BasicSchema> = BasicConfig<S>;
6
6
  /** Create a core client with the complete browser adapter set installed. */
7
7
  declare function createBasicClient<S extends BasicSchema>(config: BrowserBasicConfig<S>): BasicClient<S>;
8
8
 
9
9
  interface UseAccountsResult {
10
- accounts: BasicProfile[];
11
- active: BasicProfile | null;
10
+ accounts: BasicAccount[];
11
+ active: BasicAccount | null;
12
12
  switchAccount(id: string): Promise<void>;
13
13
  addAccount(): Promise<BasicProfile>;
14
14
  removeAccount(id: string): Promise<void>;
@@ -19,6 +19,8 @@ interface UseAuthResult {
19
19
  isReady: boolean;
20
20
  isSignedIn: boolean;
21
21
  isAnonymous: boolean;
22
+ canWrite: boolean;
23
+ readOnlyReason: string | null;
22
24
  status: AuthStatus;
23
25
  error: BasicError | null;
24
26
  user: AuthUser | null;
@@ -87,6 +89,7 @@ interface UseOutgoingSharesResult {
87
89
  get(id: string): Promise<OutgoingShareInfo>;
88
90
  cancel(id: string): Promise<OutgoingShareInfo>;
89
91
  revoke(id: string): Promise<OutgoingShareInfo>;
92
+ getContactHandle(did: string): Promise<string | null>;
90
93
  manageUrl(): string;
91
94
  }
92
95
  declare function useOutgoingShares(): UseOutgoingSharesResult;
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ __export(index_exports, {
60
60
  module.exports = __toCommonJS(index_exports);
61
61
 
62
62
  // src/create-client.ts
63
- var import_core5 = require("@basictech/core");
63
+ var import_core4 = require("@basictech/core");
64
64
 
65
65
  // src/adapters/broadcast-channel.ts
66
66
  function createBrowserMessageChannel(name) {
@@ -444,7 +444,6 @@ function browserStorage(name) {
444
444
  }
445
445
 
446
446
  // src/adapters/token-store.ts
447
- var import_core3 = require("@basictech/core");
448
447
  var ACCESS_TOKEN_ADOPTION_TIMEOUT_MS = 100;
449
448
  function parseTokens(value) {
450
449
  if (!value) return null;
@@ -521,7 +520,7 @@ var BrowserTokenStore = class {
521
520
  else this.accessWaiters.delete(key);
522
521
  }
523
522
  waitForAccessToken(key, refreshToken) {
524
- return new Promise((resolve, reject) => {
523
+ return new Promise((resolve) => {
525
524
  let timer;
526
525
  const waiter = {
527
526
  refreshToken,
@@ -537,17 +536,14 @@ var BrowserTokenStore = class {
537
536
  const remaining = (this.accessWaiters.get(key) ?? []).filter((candidate) => candidate !== waiter);
538
537
  if (remaining.length) this.accessWaiters.set(key, remaining);
539
538
  else this.accessWaiters.delete(key);
540
- reject(new import_core3.BasicError(
541
- "ACCESS_TOKEN_ADOPTION_TIMEOUT",
542
- "timed out waiting for the access token matching persisted refresh material"
543
- ));
539
+ resolve(null);
544
540
  }, ACCESS_TOKEN_ADOPTION_TIMEOUT_MS);
545
541
  });
546
542
  }
547
543
  };
548
544
 
549
545
  // src/adapters/upload.ts
550
- var import_core4 = require("@basictech/core");
546
+ var import_core3 = require("@basictech/core");
551
547
  function responseHeaders(xhr) {
552
548
  const headers = new Headers();
553
549
  for (const line of xhr.getAllResponseHeaders().trim().split(/[\r\n]+/)) {
@@ -560,7 +556,7 @@ function responseHeaders(xhr) {
560
556
  var browserUploadTransport = {
561
557
  upload(request) {
562
558
  if (typeof XMLHttpRequest === "undefined") {
563
- return Promise.reject(new import_core4.BasicError("XHR_UNAVAILABLE"));
559
+ return Promise.reject(new import_core3.BasicError("XHR_UNAVAILABLE"));
564
560
  }
565
561
  return new Promise((resolve, reject) => {
566
562
  const xhr = new XMLHttpRequest();
@@ -592,9 +588,9 @@ var browserUploadTransport = {
592
588
  statusText: xhr.statusText,
593
589
  headers: responseHeaders(xhr)
594
590
  })));
595
- xhr.onerror = () => finish(() => reject(new import_core4.BasicError("NETWORK_ERROR", "the upload could not reach the server")));
591
+ xhr.onerror = () => finish(() => reject(new import_core3.BasicError("NETWORK_ERROR", "the upload could not reach the server")));
596
592
  xhr.onabort = () => finish(() => reject(request.signal?.reason ?? new DOMException("Aborted", "AbortError")));
597
- xhr.ontimeout = () => finish(() => reject(new import_core4.BasicError("NETWORK_ERROR", "the upload timed out")));
593
+ xhr.ontimeout = () => finish(() => reject(new import_core3.BasicError("NETWORK_ERROR", "the upload timed out")));
598
594
  const form = new FormData();
599
595
  form.append("path", request.path);
600
596
  if (request.name !== void 0) form.append("name", request.name);
@@ -606,21 +602,37 @@ var browserUploadTransport = {
606
602
  };
607
603
 
608
604
  // src/create-client.ts
605
+ function browserReconcileTrigger(listener) {
606
+ if (typeof window === "undefined" || typeof document === "undefined") return () => {
607
+ };
608
+ const onVisible = () => {
609
+ if (document.visibilityState === "visible") listener();
610
+ };
611
+ window.addEventListener("focus", listener);
612
+ window.addEventListener("online", listener);
613
+ document.addEventListener("visibilitychange", onVisible);
614
+ return () => {
615
+ window.removeEventListener("focus", listener);
616
+ window.removeEventListener("online", listener);
617
+ document.removeEventListener("visibilitychange", onVisible);
618
+ };
619
+ }
609
620
  function createBasicClient(config) {
610
- const local = browserStorage("localStorage") ?? new import_core5.MemoryKeyValueStorage();
611
- const session = browserStorage("sessionStorage") ?? new import_core5.MemoryKeyValueStorage();
621
+ const local = browserStorage("localStorage") ?? new import_core4.MemoryKeyValueStorage();
622
+ const session = browserStorage("sessionStorage") ?? new import_core4.MemoryKeyValueStorage();
612
623
  const defaultTokenStore = config.tokenStore ? null : new BrowserTokenStore(local);
613
624
  const canBroadcast = typeof BroadcastChannel !== "undefined";
614
625
  const canPersistReplica = typeof indexedDB !== "undefined";
615
- return (0, import_core5.createBasicClient)({
626
+ return (0, import_core4.createBasicClient)({
616
627
  ...config,
617
628
  kv: config.kv ?? local,
618
629
  tokenStore: config.tokenStore ?? defaultTokenStore,
619
630
  sessionStorage: config.sessionStorage ?? session,
620
- replicaStore: config.replicaStore ?? (canPersistReplica ? new PersistenceStore(config.clientId) : new import_core5.MemoryReplicaStoreFactory()),
631
+ replicaStore: config.replicaStore ?? (canPersistReplica ? new PersistenceStore(config.clientId) : new import_core4.MemoryReplicaStoreFactory()),
621
632
  navigate: config.navigate ?? browserNavigate,
622
633
  currentUrl: config.currentUrl ?? browserCurrentUrl,
623
634
  replaceUrl: config.replaceUrl ?? browserReplaceUrl,
635
+ reconcileTrigger: config.reconcileTrigger ?? browserReconcileTrigger,
624
636
  createMessageChannel: config.createMessageChannel ?? (canBroadcast && defaultTokenStore ? createBrowserAuthChannelFactory(config.clientId, defaultTokenStore) : void 0),
625
637
  uploadTransport: config.uploadTransport ?? (typeof XMLHttpRequest === "undefined" ? void 0 : browserUploadTransport)
626
638
  });
@@ -706,7 +718,7 @@ var import_react4 = require("react");
706
718
 
707
719
  // src/hooks/client.ts
708
720
  var import_react3 = require("react");
709
- var import_core6 = require("@basictech/core");
721
+ var import_core5 = require("@basictech/core");
710
722
  function useRequiredClient() {
711
723
  const client = (0, import_react3.useContext)(BasicClientContext);
712
724
  if (!client) throw new Error("Basic hooks must be used within a <BasicProvider>");
@@ -716,8 +728,8 @@ function useClientSnapshot(client) {
716
728
  return (0, import_react3.useSyncExternalStore)(client.subscribe, client.getSnapshot, client.getSnapshot);
717
729
  }
718
730
  function toBasicError(error, fallback = "UNKNOWN_ERROR") {
719
- if (error instanceof import_core6.BasicError) return error;
720
- return new import_core6.BasicError(fallback, error instanceof Error ? error.message : String(error));
731
+ if (error instanceof import_core5.BasicError) return error;
732
+ return new import_core5.BasicError(fallback, error instanceof Error ? error.message : String(error));
721
733
  }
722
734
  function useAsyncResult(load, initialData, enabled, settled, dependencies) {
723
735
  const loadRef = (0, import_react3.useRef)(load);
@@ -766,12 +778,12 @@ function useAccountsFor(client) {
766
778
  const addAccount = (0, import_react4.useCallback)(() => client.addAccount(), [client]);
767
779
  const removeAccount = (0, import_react4.useCallback)((id) => client.removeAccount(id), [client]);
768
780
  return (0, import_react4.useMemo)(() => ({
769
- accounts: snapshot.profiles,
781
+ accounts: snapshot.accounts,
770
782
  active: snapshot.activeProfile,
771
783
  switchAccount,
772
784
  addAccount,
773
785
  removeAccount
774
- }), [snapshot.profiles, snapshot.activeProfile, switchAccount, addAccount, removeAccount]);
786
+ }), [snapshot.accounts, snapshot.activeProfile, switchAccount, addAccount, removeAccount]);
775
787
  }
776
788
  function useAccounts() {
777
789
  return useAccountsFor(useRequiredClient());
@@ -779,7 +791,7 @@ function useAccounts() {
779
791
 
780
792
  // src/hooks/auth.ts
781
793
  var import_react5 = require("react");
782
- var import_core7 = require("@basictech/core");
794
+ var import_core6 = require("@basictech/core");
783
795
  function useAuthFor(client) {
784
796
  const snapshot = useClientSnapshot(client);
785
797
  const signIn = (0, import_react5.useCallback)((input) => client.signIn(input), [client]);
@@ -789,8 +801,10 @@ function useAuthFor(client) {
789
801
  isReady: snapshot.isReady,
790
802
  isSignedIn: snapshot.isSignedIn,
791
803
  isAnonymous: snapshot.isAnonymous,
804
+ canWrite: snapshot.canWrite,
805
+ readOnlyReason: snapshot.readOnlyReason,
792
806
  status: snapshot.authStatus,
793
- error: snapshot.authErrorCode ? new import_core7.BasicError(snapshot.authErrorCode) : null,
807
+ error: snapshot.authErrorCode ? new import_core6.BasicError(snapshot.authErrorCode) : null,
794
808
  user: snapshot.user,
795
809
  did: snapshot.did,
796
810
  handle: snapshot.handle,
@@ -979,6 +993,7 @@ function useOutgoingSharesFor(client) {
979
993
  const get = (0, import_react9.useCallback)((id) => client.shares.getOutgoing(id), [client]);
980
994
  const cancel = (0, import_react9.useCallback)((id) => client.shares.cancel(id), [client]);
981
995
  const revoke = (0, import_react9.useCallback)((id) => client.shares.revoke(id), [client]);
996
+ const getContactHandle = (0, import_react9.useCallback)((did) => client.shares.getContactHandle(did), [client]);
982
997
  const manageUrl = (0, import_react9.useCallback)(() => client.shares.manageUrl(), [client]);
983
998
  const { data: outgoingShares, ...state } = result;
984
999
  return {
@@ -989,6 +1004,7 @@ function useOutgoingSharesFor(client) {
989
1004
  get,
990
1005
  cancel,
991
1006
  revoke,
1007
+ getContactHandle,
992
1008
  manageUrl
993
1009
  };
994
1010
  }