jazz-react-native 0.8.51 → 0.9.0

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,205 @@
1
+ import {
2
+ Account,
3
+ AgentID,
4
+ AnonymousJazzAgent,
5
+ AuthMethod,
6
+ CoValue,
7
+ CoValueClass,
8
+ CryptoProvider,
9
+ ID,
10
+ InviteSecret,
11
+ SessionID,
12
+ cojsonInternals,
13
+ createJazzContext,
14
+ } from "jazz-tools";
15
+
16
+ import { RawAccountID } from "cojson";
17
+
18
+ export { RNDemoAuth } from "./auth/DemoAuthMethod.js";
19
+
20
+ import { PureJSCrypto } from "cojson/native";
21
+ import { createWebSocketPeerWithReconnection } from "./createWebSocketPeerWithReconnection.js";
22
+ import type { RNQuickCrypto } from "./crypto/RNQuickCrypto.js";
23
+ import { ExpoSecureStoreAdapter } from "./storage/expo-secure-store-adapter.js";
24
+ import { KvStoreContext } from "./storage/kv-store-context.js";
25
+
26
+ /** @category Context Creation */
27
+ export type ReactNativeContext<Acc extends Account> = {
28
+ me: Acc;
29
+ logOut: () => void;
30
+ // TODO: Symbol.dispose?
31
+ done: () => void;
32
+ };
33
+
34
+ export type ReactNativeGuestContext = {
35
+ guest: AnonymousJazzAgent;
36
+ logOut: () => void;
37
+ done: () => void;
38
+ };
39
+
40
+ export type ReactNativeContextOptions<Acc extends Account> = {
41
+ auth: AuthMethod;
42
+ AccountSchema: CoValueClass<Acc> & {
43
+ fromNode: (typeof Account)["fromNode"];
44
+ };
45
+ } & BaseReactNativeContextOptions;
46
+
47
+ export type BaseReactNativeContextOptions = {
48
+ peer: `wss://${string}` | `ws://${string}`;
49
+ reconnectionTimeout?: number;
50
+ storage?: "indexedDB" | "singleTabOPFS";
51
+ CryptoProvider?: typeof PureJSCrypto | typeof RNQuickCrypto;
52
+ };
53
+
54
+ /** @category Context Creation */
55
+ export async function createJazzRNContext<Acc extends Account>(
56
+ options: ReactNativeContextOptions<Acc>,
57
+ ): Promise<ReactNativeContext<Acc>>;
58
+ export async function createJazzRNContext(
59
+ options: BaseReactNativeContextOptions,
60
+ ): Promise<ReactNativeGuestContext>;
61
+ export async function createJazzRNContext<Acc extends Account>(
62
+ options: ReactNativeContextOptions<Acc> | BaseReactNativeContextOptions,
63
+ ): Promise<ReactNativeContext<Acc> | ReactNativeGuestContext>;
64
+ export async function createJazzRNContext<Acc extends Account>(
65
+ options: ReactNativeContextOptions<Acc> | BaseReactNativeContextOptions,
66
+ ): Promise<ReactNativeContext<Acc> | ReactNativeGuestContext> {
67
+ const websocketPeer = createWebSocketPeerWithReconnection(
68
+ options.peer,
69
+ options.reconnectionTimeout,
70
+ (peer) => {
71
+ node.syncManager.addPeer(peer);
72
+ },
73
+ );
74
+
75
+ const CryptoProvider = options.CryptoProvider || PureJSCrypto;
76
+
77
+ const context =
78
+ "auth" in options
79
+ ? await createJazzContext({
80
+ AccountSchema: options.AccountSchema,
81
+ auth: options.auth,
82
+ crypto: await CryptoProvider.create(),
83
+ peersToLoadFrom: [websocketPeer.peer],
84
+ sessionProvider: provideLockSession,
85
+ })
86
+ : await createJazzContext({
87
+ crypto: await CryptoProvider.create(),
88
+ peersToLoadFrom: [websocketPeer.peer],
89
+ });
90
+
91
+ const node =
92
+ "account" in context ? context.account._raw.core.node : context.agent.node;
93
+
94
+ return "account" in context
95
+ ? {
96
+ me: context.account,
97
+ done: () => {
98
+ websocketPeer.done();
99
+ context.done();
100
+ },
101
+ logOut: () => {
102
+ context.logOut();
103
+ },
104
+ }
105
+ : {
106
+ guest: context.agent,
107
+ done: () => {
108
+ websocketPeer.done();
109
+ context.done();
110
+ },
111
+ logOut: () => {
112
+ context.logOut();
113
+ },
114
+ };
115
+ }
116
+
117
+ /** @category Auth Providers */
118
+ export type SessionProvider = (
119
+ accountID: ID<Account> | AgentID,
120
+ ) => Promise<SessionID>;
121
+
122
+ export async function provideLockSession(
123
+ accountID: ID<Account> | AgentID,
124
+ crypto: CryptoProvider,
125
+ ) {
126
+ const sessionDone = () => {};
127
+
128
+ const kvStore = KvStoreContext.getInstance().getStorage();
129
+
130
+ const sessionID =
131
+ ((await kvStore.get(accountID)) as SessionID) ||
132
+ crypto.newRandomSessionID(accountID as RawAccountID | AgentID);
133
+ await kvStore.set(accountID, sessionID);
134
+
135
+ return Promise.resolve({
136
+ sessionID,
137
+ sessionDone,
138
+ });
139
+ }
140
+
141
+ /** @category Invite Links */
142
+ export function createInviteLink<C extends CoValue>(
143
+ value: C,
144
+ role: "reader" | "writer" | "admin",
145
+ { baseURL, valueHint }: { baseURL?: string; valueHint?: string } = {},
146
+ ): string {
147
+ const coValueCore = value._raw.core;
148
+ let currentCoValue = coValueCore;
149
+
150
+ while (currentCoValue.header.ruleset.type === "ownedByGroup") {
151
+ currentCoValue = currentCoValue.getGroup().core;
152
+ }
153
+
154
+ if (currentCoValue.header.ruleset.type !== "group") {
155
+ throw new Error("Can't create invite link for object without group");
156
+ }
157
+
158
+ const group = cojsonInternals.expectGroup(currentCoValue.getCurrentContent());
159
+ const inviteSecret = group.createInvite(role);
160
+
161
+ return `${baseURL}/invite/${valueHint ? valueHint + "/" : ""}${
162
+ value.id
163
+ }/${inviteSecret}`;
164
+ }
165
+
166
+ /** @category Invite Links */
167
+ // TODO: copied from jazz-browser, should be shared
168
+ export function parseInviteLink<C extends CoValue>(
169
+ inviteURL: string,
170
+ ):
171
+ | {
172
+ valueID: ID<C>;
173
+ valueHint?: string;
174
+ inviteSecret: InviteSecret;
175
+ }
176
+ | undefined {
177
+ const url = new URL(inviteURL);
178
+ const parts = url.hash.split("/");
179
+
180
+ let valueHint: string | undefined;
181
+ let valueID: ID<C> | undefined;
182
+ let inviteSecret: InviteSecret | undefined;
183
+
184
+ if (parts[0] === "#" && parts[1] === "invite") {
185
+ if (parts.length === 5) {
186
+ valueHint = parts[2];
187
+ valueID = parts[3] as ID<C>;
188
+ inviteSecret = parts[4] as InviteSecret;
189
+ } else if (parts.length === 4) {
190
+ valueID = parts[2] as ID<C>;
191
+ inviteSecret = parts[3] as InviteSecret;
192
+ }
193
+
194
+ if (!valueID || !inviteSecret) {
195
+ return undefined;
196
+ }
197
+ return { valueID, inviteSecret, valueHint };
198
+ }
199
+ }
200
+
201
+ export function setupKvStore(kvStore = new ExpoSecureStoreAdapter()) {
202
+ KvStoreContext.getInstance().initialize(kvStore);
203
+
204
+ return kvStore;
205
+ }
package/src/provider.tsx CHANGED
@@ -1,314 +1,109 @@
1
- import React, { useEffect, useState } from "react";
2
-
3
- import {
4
- Account,
5
- AccountClass,
6
- AnonymousJazzAgent,
7
- AuthMethod,
8
- CoValue,
9
- CoValueClass,
10
- DeeplyLoaded,
11
- DepthsIn,
12
- ID,
13
- subscribeToCoValue,
14
- } from "jazz-tools";
15
- import { Linking } from "react-native";
1
+ import { JazzContext, JazzContextType } from "jazz-react-core";
2
+ import { Account, AccountClass, AuthMethod } from "jazz-tools";
3
+ import { useState } from "react";
4
+ import { useEffect, useRef } from "react";
16
5
  import {
17
6
  BaseReactNativeContextOptions,
18
- KvStore,
19
- KvStoreContext,
20
- ReactNativeContext,
21
- ReactNativeGuestContext,
22
7
  createJazzRNContext,
23
- parseInviteLink,
24
- } from "./index.js";
25
- import { ExpoSecureStoreAdapter } from "./storage/expo-secure-store-adapter.js";
8
+ } from "./platform.js";
9
+ export interface Register {}
26
10
 
27
- /** @category Context & Hooks */
28
- export function createJazzRNApp<Acc extends Account>({
29
- kvStore = new ExpoSecureStoreAdapter(),
30
- AccountSchema = Account as unknown as AccountClass<Acc>,
31
- CryptoProvider,
32
- }: {
33
- kvStore?: KvStore;
11
+ export type RegisteredAccount = Register extends { Account: infer Acc }
12
+ ? Acc
13
+ : Account;
14
+
15
+ export type JazzProviderProps<Acc extends Account = RegisteredAccount> = {
16
+ children: React.ReactNode;
17
+ auth: AuthMethod | "guest";
18
+ peer: `wss://${string}` | `ws://${string}`;
34
19
  AccountSchema?: AccountClass<Acc>;
35
20
  CryptoProvider?: BaseReactNativeContextOptions["CryptoProvider"];
36
- } = {}): JazzReactApp<Acc> {
37
- const JazzContext = React.createContext<
38
- ReactNativeContext<Acc> | ReactNativeGuestContext | undefined
39
- >(undefined);
40
-
41
- if (!kvStore) {
42
- throw new Error("kvStore is required");
43
- }
21
+ };
44
22
 
45
- KvStoreContext.getInstance().initialize(kvStore);
46
-
47
- function Provider({
48
- children,
49
- auth,
50
- peer,
51
- storage,
52
- }: {
53
- children: React.ReactNode;
54
- auth: AuthMethod | "guest";
55
- peer: `wss://${string}` | `ws://${string}`;
56
- storage?: "indexedDB" | "singleTabOPFS";
57
- }) {
58
- const [ctx, setCtx] = useState<
59
- ReactNativeContext<Acc> | ReactNativeGuestContext | undefined
60
- >();
61
-
62
- const [sessionCount, setSessionCount] = useState(0);
23
+ /** @category Context & Hooks */
24
+ export function JazzProvider<Acc extends Account = RegisteredAccount>({
25
+ children,
26
+ auth,
27
+ peer,
28
+ AccountSchema = Account as unknown as AccountClass<Acc>,
29
+ CryptoProvider,
30
+ }: JazzProviderProps<Acc>) {
31
+ const [ctx, setCtx] = useState<JazzContextType<Acc> | undefined>();
32
+
33
+ const [sessionCount, setSessionCount] = useState(0);
34
+
35
+ const effectExecuted = useRef(false);
36
+ effectExecuted.current = false;
37
+
38
+ useEffect(() => {
39
+ // Avoid double execution of the effect in development mode for easier debugging.
40
+ if (process.env.NODE_ENV === "development") {
41
+ if (effectExecuted.current) {
42
+ return;
43
+ }
44
+ effectExecuted.current = true;
45
+
46
+ // In development mode we don't return a cleanup function because otherwise
47
+ // the double effect execution would mark the context as done immediately.
48
+ //
49
+ // So we mark it as done in the subsequent execution.
50
+ const previousContext = ctx;
51
+
52
+ if (previousContext) {
53
+ previousContext.done();
54
+ }
55
+ }
63
56
 
64
- useEffect(() => {
65
- const promiseWithDoneCallback = createJazzRNContext<Acc>(
57
+ async function createContext() {
58
+ const currentContext = await createJazzRNContext<Acc>(
66
59
  auth === "guest"
67
60
  ? {
68
61
  peer,
69
- storage,
70
62
  CryptoProvider,
71
63
  }
72
64
  : {
73
65
  AccountSchema,
74
66
  auth: auth,
75
67
  peer,
76
- storage,
77
68
  CryptoProvider,
78
69
  },
79
- ).then((context) => {
80
- setCtx({
81
- ...context,
82
- logOut: () => {
83
- context.logOut();
84
- setCtx(undefined);
85
- setSessionCount(sessionCount + 1);
86
- },
87
- });
88
- return context.done;
89
- });
90
-
91
- return () => {
92
- void promiseWithDoneCallback
93
- .then((done) => done())
94
- .catch((e) => {
95
- console.error("Error in createJazzRNContext", e);
96
- });
97
- };
98
- }, [AccountSchema, auth, peer, storage, sessionCount]);
99
-
100
- return (
101
- <JazzContext.Provider value={ctx}>{ctx && children}</JazzContext.Provider>
102
- );
103
- }
104
-
105
- function useAccount(): { me: Acc; logOut: () => void };
106
- function useAccount<D extends DepthsIn<Acc>>(
107
- depth: D,
108
- ): { me: DeeplyLoaded<Acc, D> | undefined; logOut: () => void };
109
- function useAccount<D extends DepthsIn<Acc>>(
110
- depth?: D,
111
- ): { me: Acc | DeeplyLoaded<Acc, D> | undefined; logOut: () => void } {
112
- const context = React.useContext(JazzContext);
113
-
114
- if (!context) {
115
- throw new Error("useAccount must be used within a JazzProvider");
116
- }
117
-
118
- if (!("me" in context)) {
119
- throw new Error(
120
- "useAccount can't be used in a JazzProvider with auth === 'guest' - consider using useAccountOrGuest()",
121
70
  );
122
- }
123
-
124
- const me = useCoState<Acc, D>(
125
- context?.me.constructor as CoValueClass<Acc>,
126
- context?.me.id,
127
- depth,
128
- );
129
-
130
- return {
131
- me: depth === undefined ? me || context.me : me,
132
- logOut: context.logOut,
133
- };
134
- }
135
-
136
- function useAccountOrGuest(): { me: Acc | AnonymousJazzAgent };
137
- function useAccountOrGuest<D extends DepthsIn<Acc>>(
138
- depth: D,
139
- ): { me: DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent };
140
- function useAccountOrGuest<D extends DepthsIn<Acc>>(
141
- depth?: D,
142
- ): { me: Acc | DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent } {
143
- const context = React.useContext(JazzContext);
144
-
145
- if (!context) {
146
- throw new Error("useAccountOrGuest must be used within a JazzProvider");
147
- }
148
71
 
149
- const contextMe = "me" in context ? context.me : undefined;
72
+ const logOut = () => {
73
+ currentContext.logOut();
74
+ setCtx(undefined);
75
+ setSessionCount(sessionCount + 1);
150
76
 
151
- const me = useCoState<Acc, D>(
152
- contextMe?.constructor as CoValueClass<Acc>,
153
- contextMe?.id,
154
- depth,
155
- );
156
-
157
- if ("me" in context) {
158
- return {
159
- me: depth === undefined ? me || context.me : me,
160
- };
161
- } else {
162
- return { me: context.guest };
163
- }
164
- }
165
-
166
- function useCoState<V extends CoValue, D>(
167
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
- Schema: CoValueClass<V>,
169
- id: ID<V> | undefined,
170
- depth: D & DepthsIn<V> = [] as D & DepthsIn<V>,
171
- ): DeeplyLoaded<V, D> | undefined {
172
- const [state, setState] = useState<{
173
- value: DeeplyLoaded<V, D> | undefined;
174
- }>({ value: undefined });
175
- const context = React.useContext(JazzContext);
176
-
177
- if (!context) {
178
- throw new Error("useCoState must be used within a JazzProvider");
179
- }
180
-
181
- useEffect(() => {
182
- if (!id) return;
183
-
184
- return subscribeToCoValue(
185
- Schema,
186
- id,
187
- "me" in context ? context.me : context.guest,
188
- depth,
189
- (value) => {
190
- setState({ value });
191
- },
192
- );
193
- }, [Schema, id, context]);
194
-
195
- return state.value;
196
- }
197
-
198
- function useAcceptInvite<V extends CoValue>({
199
- invitedObjectSchema,
200
- onAccept,
201
- forValueHint,
202
- }: {
203
- invitedObjectSchema: CoValueClass<V>;
204
- onAccept: (projectID: ID<V>) => void;
205
- forValueHint?: string;
206
- }): void {
207
- const context = React.useContext(JazzContext);
208
-
209
- if (!context) {
210
- throw new Error("useAcceptInvite must be used within a JazzProvider");
211
- }
212
-
213
- if (!("me" in context)) {
214
- throw new Error(
215
- "useAcceptInvite can't be used in a JazzProvider with auth === 'guest'.",
216
- );
217
- }
218
-
219
- useEffect(() => {
220
- const handleDeepLink = ({ url }: { url: string }) => {
221
- const result = parseInviteLink<V>(url);
222
- if (result && result.valueHint === forValueHint) {
223
- context.me
224
- .acceptInvite(
225
- result.valueID,
226
- result.inviteSecret,
227
- invitedObjectSchema,
228
- )
229
- .then(() => {
230
- onAccept(result.valueID);
231
- })
232
- .catch((e) => {
233
- console.error("Failed to accept invite", e);
234
- });
77
+ if (process.env.NODE_ENV === "development") {
78
+ // In development mode we don't return a cleanup function
79
+ // so we mark the context as done here.
80
+ currentContext.done();
235
81
  }
236
82
  };
237
83
 
238
- const linkingListener = Linking.addEventListener("url", handleDeepLink);
239
-
240
- void Linking.getInitialURL().then((url) => {
241
- if (url) handleDeepLink({ url });
84
+ setCtx({
85
+ ...currentContext,
86
+ AccountSchema,
87
+ logOut,
242
88
  });
243
89
 
244
- return () => {
245
- linkingListener.remove();
246
- };
247
- }, [context, onAccept, invitedObjectSchema, forValueHint]);
248
- }
249
-
250
- return {
251
- Provider,
252
- useAccount,
253
- useAccountOrGuest,
254
- useCoState,
255
- useAcceptInvite,
256
- kvStore,
257
- };
258
- }
259
-
260
- /** @category Context & Hooks */
261
- export interface JazzReactApp<Acc extends Account> {
262
- /** @category Provider Component */
263
- Provider: React.FC<{
264
- children: React.ReactNode;
265
- auth: AuthMethod | "guest";
266
- peer: `wss://${string}` | `ws://${string}`;
267
- storage?: "indexedDB" | "singleTabOPFS";
268
- }>;
90
+ return currentContext;
91
+ }
269
92
 
270
- /** @category Hooks */
271
- useAccount(): {
272
- me: Acc;
273
- logOut: () => void;
274
- };
275
- /** @category Hooks */
276
- useAccount<D extends DepthsIn<Acc>>(
277
- depth: D,
278
- ): {
279
- me: DeeplyLoaded<Acc, D> | undefined;
280
- logOut: () => void;
281
- };
93
+ const promise = createContext();
282
94
 
283
- /** @category Hooks */
284
- useAccountOrGuest(): {
285
- me: Acc | AnonymousJazzAgent;
286
- };
287
- useAccountOrGuest<D extends DepthsIn<Acc>>(
288
- depth: D,
289
- ): {
290
- me: DeeplyLoaded<Acc, D> | undefined | AnonymousJazzAgent;
291
- };
292
- /** @category Hooks */
293
- useCoState<V extends CoValue, D>(
294
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
295
- Schema: { new (...args: any[]): V } & CoValueClass,
296
- id: ID<V> | undefined,
297
- depth?: D & DepthsIn<V>,
298
- ): DeeplyLoaded<V, D> | undefined;
95
+ // In development mode we don't return a cleanup function because otherwise
96
+ // the double effect execution would mark the context as done immediately.
97
+ if (process.env.NODE_ENV === "development") {
98
+ return;
99
+ }
299
100
 
300
- /** @category Hooks */
301
- useAcceptInvite<V extends CoValue>({
302
- invitedObjectSchema,
303
- onAccept,
304
- forValueHint,
305
- }: {
306
- invitedObjectSchema: CoValueClass<V>;
307
- onAccept: (projectID: ID<V>) => void;
308
- forValueHint?: string;
309
- }): void;
101
+ return () => {
102
+ void promise.then((context) => context.done());
103
+ };
104
+ }, [AccountSchema, auth, peer, sessionCount]);
310
105
 
311
- kvStore: KvStore;
106
+ return (
107
+ <JazzContext.Provider value={ctx}>{ctx && children}</JazzContext.Provider>
108
+ );
312
109
  }
313
-
314
- export * from "./media.js";
@@ -18,6 +18,10 @@ export class KvStoreContext {
18
18
  return KvStoreContext.instance;
19
19
  }
20
20
 
21
+ public isInitialized(): boolean {
22
+ return this.storageInstance !== null;
23
+ }
24
+
21
25
  public initialize(store: KvStore): void {
22
26
  if (!this.storageInstance) {
23
27
  this.storageInstance = store;
@@ -0,0 +1 @@
1
+ export * from "jazz-react-core/testing";