Skip to content

Commit 8c588e4

Browse files
authored
Ml/onboarding single user + tons of refactoring account / client handling logic (#1598)
1 parent 271b84c commit 8c588e4

File tree

193 files changed

+3855
-4658
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

193 files changed

+3855
-4658
lines changed

.cursorrules

+143-11
Original file line numberDiff line numberDiff line change
@@ -146,19 +146,151 @@ import { IConversationMembershipSearchResult } from "../../../features/search/se
146146

147147
## React Query Principles
148148

149-
- Avoid destructuring rest operators with useQuery.
149+
## React Query Best Practices
150150

151-
- Be explicit about which properties you need from useQuery
152-
- to prevent excessive re-rendering and performance issues.
153-
-
154-
- @example
155-
- // ❌ Bad: Causes unnecessary re-renders
156-
- const { data: conversations, ...rest } = useQuery()
157-
-
158-
- // ✅ Good: Explicit property usage
159-
- const { data: conversations, isLoading, error } = useQuery()
151+
### Query Key Management
152+
153+
- All query keys must be defined in `QueryKeys.ts` using factory functions
154+
- Follow naming pattern: `<entity>QueryKey` for key factories
155+
- Use query key factory functions from `QueryKeys.ts` when accessing query keys
156+
157+
@example
158+
// queries/QueryKeys.ts
159+
export const conversationsQueryKey = (account: string) => [
160+
QueryKeys.CONVERSATIONS,
161+
account?.toLowerCase(),
162+
];
163+
164+
// queries/use-conversations-query.ts
165+
export const getConversationsQueryOptions = (args: IArgs) => ({
166+
queryKey: conversationsQueryKey(args.account),
167+
queryFn: () => getConversations(args),
168+
enabled: !!args.account
169+
});
170+
171+
### Query Options Encapsulation
172+
173+
- Use React Query's `queryOptions` helper for type safety
174+
- Export query options generators for cross-component usage
175+
- Handle conditional fetching with `enabled` and `skipToken`
176+
- Create an `enabled` variable before returning query options to avoid duplicating logic
177+
-
178+
179+
@example
180+
// queries/use-conversation-messages-query.ts
181+
import { queryOptions, skipToken } from "@tanstack/react-query";
182+
export const getConversationMessagesQueryOptions = (args: {
183+
account: string;
184+
topic: ConversationTopic;
185+
caller?: string;
186+
}) => {
187+
const enabled = !!args.account && !!args.topic;
188+
return queryOptions({
189+
queryKey: conversationMessagesQueryKey(args.account, args.topic),
190+
queryFn: enabled ? () => conversationMessagesQueryFn(args) : skipToken,
191+
enabled,
192+
});
193+
};
194+
195+
### Mutation Patterns
196+
197+
- Follow mutation structure with optimistic updates and error rollbacks
198+
- Use service functions for API calls
199+
- Maintain query cache consistency through direct updates
200+
201+
@example
202+
// features/conversation-list/hooks/use-pin-or-unpin-conversation.tsx
203+
const { mutateAsync: pinConversationAsync } = useMutation({
204+
mutationFn: () =>
205+
pinTopicService({
206+
account: currentAccount,
207+
topic: conversationTopic,
208+
}),
209+
210+
onMutate: async () => {
211+
const previousData = getConversationMetadataQueryData({
212+
account,
213+
topic
214+
});
215+
updateConversationMetadataOptimistically({ isPinned: true });
216+
return { previousData };
217+
},
218+
219+
onError: (error, variables, context) => {
220+
logger.error(`Failed to pin conversation: ${error}`);
221+
rollbackConversationMetadata(context.previousData);
222+
},
223+
224+
onSettled: () => {
225+
queryClient.invalidateQueries({
226+
queryKey: conversationMetadataQueryKey(account, topic)
227+
});
228+
}
229+
});
230+
231+
// Service function in api/conversations.ts
232+
export const pinTopicService = async (args: {
233+
account: string;
234+
topic: string;
235+
}) => {
236+
const response = await fetch(`${API_URL}/pin`, {
237+
method: "POST",
238+
body: JSON.stringify(args),
239+
});
240+
return PinResponseSchema.parse(await response.json());
241+
};
242+
243+
### Cache Management
244+
245+
- Use atomic updates with queryClient methods
246+
- Export cache update helpers with their related query
247+
- Access query keys through query options getters rather than directly using key factories
248+
- Let TypeScript infer types from query options instead of manually specifying generics
249+
250+
@example
251+
// queries/use-conversations-query.ts
252+
export function updateConversationInCache(args: {
253+
account: string;
254+
topic: string;
255+
update: Partial<ConversationType>;
256+
}) {
257+
const queryOptions = getConversationsQueryOptions({ account: args.account });
258+
259+
queryClient.setQueryData<ConversationType[]>(
260+
queryOptions.queryKey,
261+
(prev) => prev?.map(conv =>
262+
conv.topic === args.topic ? { ...conv, ...args.update } : conv
263+
)
264+
);
265+
}
266+
267+
### Query Key Access
268+
269+
- Always retrieve query keys through their associated query options
270+
- Avoid direct usage of query key factories in cache management functions
271+
- Leverage query options' built-in type inference instead of manual type assertions
272+
273+
@example
274+
// Correct pattern using query options
275+
const options = getConversationsQueryOptions({ account });
276+
queryClient.getQueryData(options.queryKey);
277+
278+
// ❌ Avoid direct key factory usage
279+
queryClient.getQueryData(conversationsQueryKey(account));
280+
281+
## React Query Implementation Principles
282+
283+
- Avoid destructuring rest operators with useQuery
284+
- Be explicit about which properties you need from useQuery - to prevent excessive re-rendering and performance issues.
285+
-
286+
- @example
287+
- // ❌ Bad: Causes unnecessary re-renders
288+
- const { data: conversations, ...rest } = useQuery()
289+
-
290+
- // ✅ Good: Explicit property usage
291+
- const { data: conversations, isLoading, error } = useQuery()
160292

161-
- Use object parameters for query options.
293+
* Use object parameters for query options.
162294
- Pass query options as objects for better maintainability
163295
- and clearer intention.
164296
-

App.tsx

+84-60
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,32 @@
11
import { PrivyProvider } from "@privy-io/expo";
2-
2+
import { DevToolsBubble } from "react-native-react-query-devtools";
3+
import * as Clipboard from "expo-clipboard";
34
// This is a requirement for Privy to work, does not make any sense
45
// To test run yarn start --no-dev --minify
56

6-
import { setupStreamingSubscriptions } from "@/features/streams/streams";
77
import { configure as configureCoinbase } from "@coinbase/wallet-mobile-sdk";
8-
import DebugButton from "@components/DebugButton";
8+
import { DebugButton } from "@components/DebugButton";
9+
import {
10+
AppState,
11+
Platform,
12+
StyleSheet,
13+
View,
14+
useColorScheme,
15+
} from "react-native";
916
import { Snackbars } from "@components/Snackbar/Snackbars";
1017
import { BottomSheetModalProvider } from "@design-system/BottomSheet/BottomSheetModalProvider";
1118
import { useReactQueryDevTools } from "@dev-plugins/react-query";
1219
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
13-
import { useAppStateHandlers } from "@hooks/useAppStateHandlers";
1420
import { SmartWalletsProvider } from "@privy-io/expo/smart-wallets";
1521
import { queryClient } from "@queries/queryClient";
1622
import { MaterialDarkTheme, MaterialLightTheme } from "@styles/colors";
17-
import { QueryClientProvider } from "@tanstack/react-query";
23+
import { focusManager, QueryClientProvider } from "@tanstack/react-query";
1824
import { useThemeProvider } from "@theme/useAppTheme";
1925
import { setupAppAttest } from "@utils/appCheck";
2026
import { useCoinbaseWalletListener } from "@utils/coinbaseWallet";
2127
import { converseEventEmitter } from "@utils/events";
22-
import logger from "@utils/logger";
2328
import "expo-dev-client";
2429
import React, { useCallback, useEffect, useMemo, useRef } from "react";
25-
import {
26-
LogBox,
27-
Platform,
28-
StyleSheet,
29-
View,
30-
useColorScheme,
31-
} from "react-native";
3230
import { GestureHandlerRootView } from "react-native-gesture-handler";
3331
import { KeyboardProvider } from "react-native-keyboard-controller";
3432
import { Provider as PaperProvider } from "react-native-paper";
@@ -38,35 +36,60 @@ import {
3836
} from "react-native-reanimated";
3937
import { ThirdwebProvider } from "thirdweb/react";
4038
import { config } from "./config";
41-
import {
42-
TEMPORARY_ACCOUNT_NAME,
43-
useAccountsStore,
44-
} from "./data/store/accountsStore";
45-
import { setAuthStatus } from "./data/store/authStore";
46-
import Main from "./screens/Main";
47-
import { registerBackgroundFetchTask } from "./utils/background";
39+
import { Main } from "./screens/Main";
4840
import { initSentry } from "./utils/sentry";
4941
import { saveApiURI } from "./utils/sharedData";
5042
import { preventSplashScreenAutoHide } from "./utils/splash/splash";
51-
52-
preventSplashScreenAutoHide();
53-
54-
LogBox.ignoreLogs([
55-
"Privy: Expected status code 200, received 400", // Privy
56-
"Error destroying session", // Privy
57-
'event="noNetwork', // ethers
58-
"[Reanimated] Reading from `value` during component render. Please ensure that you do not access the `value` property or use `get` method of a shared value while React is rendering a component.",
59-
"Attempted to import the module", // General module import warnings
60-
'Attempted to import the module "/Users', // More specific module import warnings
61-
"Falling back to file-based resolution. Consider updating the call site or asking the package maintainer(s) to expose this API",
62-
"Couldn't find real values for `KeyboardContext`. Please make sure you're inside of `KeyboardProvider` - otherwise functionality of `react-native-keyboard-controller` will not work. [Component Stack]",
43+
import { setupStreamingSubscriptions } from "@/features/streams/streams";
44+
import {
45+
// MultiInboxClient,
46+
useInitializeMultiInboxClient,
47+
} from "@/features/multi-inbox/multi-inbox.client";
48+
// import { useAppStateHandlers } from "./hooks/useAppStateHandlers";
49+
// import { useInstalledWallets } from "@/features/wallets/use-installed-wallets.hook";
50+
import logger from "./utils/logger";
51+
// import { useAccountsStore } from "./features/multi-inbox/multi-inbox.store";
52+
// import { AuthenticateWithPasskeyProvider } from "./features/onboarding/contexts/signup-with-passkey.context";
53+
// import { PrivyPlaygroundLandingScreen } from "./features/privy-playground/privy-playground-landing.screen";
54+
55+
!!preventSplashScreenAutoHide && preventSplashScreenAutoHide();
56+
57+
const IGNORED_LOGS = [
58+
"Couldn't find real values for `KeyboardContext",
59+
"Error destroying session",
60+
'event="noNetwork',
61+
"[Reanimated] Reading from `value` during component render",
62+
"Attempted to import the module",
63+
'Attempted to import the module "/Users',
64+
"Falling back to file-based resolution",
6365
"sync worker error storage error: Pool needs to reconnect before use",
64-
"[Converse.debug.dylib] sync worker error storage error: Pool needs to reconnect before use",
65-
"Falling back to file-based resolution. Consider updating the call site or asking the package maintainer(s) to expose this API.",
66-
]);
66+
"Require cycle", // This will catch all require cycle warnings
67+
];
6768

69+
// Workaround for console filtering in development
6870
if (__DEV__) {
69-
require("./ReactotronConfig.ts");
71+
const connectConsoleTextFromArgs = (arrayOfStrings: string[]): string =>
72+
arrayOfStrings
73+
.slice(1)
74+
.reduce(
75+
(baseString, currentString) => baseString.replace("%s", currentString),
76+
arrayOfStrings[0]
77+
);
78+
79+
const filterIgnoredMessages =
80+
(consoleLog: typeof console.log) =>
81+
(...args: any[]) => {
82+
const output = connectConsoleTextFromArgs(args);
83+
84+
if (!IGNORED_LOGS.some((log) => output.includes(log))) {
85+
consoleLog(...args);
86+
}
87+
};
88+
89+
console.log = filterIgnoredMessages(console.log);
90+
console.info = filterIgnoredMessages(console.info);
91+
console.warn = filterIgnoredMessages(console.warn);
92+
console.error = filterIgnoredMessages(console.error);
7093
}
7194

7295
// This is the default configuration
@@ -101,12 +124,9 @@ const App = () => {
101124
setupStreamingSubscriptions();
102125
}, []);
103126

127+
const coinbaseUrl = new URL(`https://${config.websiteDomain}/coinbase`);
104128
useCoinbaseWalletListener(true, coinbaseUrl);
105129

106-
useEffect(() => {
107-
registerBackgroundFetchTask();
108-
}, []);
109-
110130
const showDebugMenu = useCallback(() => {
111131
if (!debugRef.current || !(debugRef.current as any).showDebugMenu) {
112132
return;
@@ -120,28 +140,14 @@ const App = () => {
120140
converseEventEmitter.off("showDebugMenu", showDebugMenu);
121141
};
122142
}, [showDebugMenu]);
123-
124-
// For now we use persit with zustand to get the accounts when the app launch so here is okay to see if we're logged in or not
125143
useEffect(() => {
126-
const currentAccount = useAccountsStore.getState().currentAccount;
127-
if (currentAccount && currentAccount !== TEMPORARY_ACCOUNT_NAME) {
128-
setAuthStatus("signedIn");
129-
} else {
130-
setAuthStatus("signedOut");
131-
}
144+
AppState.addEventListener("change", (state) => {
145+
logger.debug("[App] AppState changed to", state);
146+
focusManager.setFocused(state === "active");
147+
});
132148
}, []);
133149

134-
useAppStateHandlers({
135-
onBackground() {
136-
logger.debug("App is in background");
137-
},
138-
onForeground() {
139-
logger.debug("App is in foreground");
140-
},
141-
onInactive() {
142-
logger.debug("App is inactive");
143-
},
144-
});
150+
// For now we use persit with zustand to get the accounts when the app launch so here is okay to see if we're logged in or not
145151

146152
return (
147153
<View style={styles.safe}>
@@ -154,8 +160,10 @@ const App = () => {
154160
// On Android we use the default keyboard "animation"
155161
const AppKeyboardProvider =
156162
Platform.OS === "ios" ? KeyboardProvider : React.Fragment;
163+
// import { DevToolsBubble } from "react-native-react-query-devtools";
157164

158-
export default function AppWithProviders() {
165+
export function AppWithProviders() {
166+
useInitializeMultiInboxClient();
159167
const colorScheme = useColorScheme();
160168

161169
const paperTheme = useMemo(() => {
@@ -167,6 +175,18 @@ export default function AppWithProviders() {
167175
const { themeScheme, setThemeContextOverride, ThemeProvider } =
168176
useThemeProvider();
169177

178+
const onCopy = async (text: string) => {
179+
try {
180+
// For Expo:
181+
await Clipboard.setStringAsync(text);
182+
// OR for React Native CLI:
183+
// await Clipboard.setString(text);
184+
return true;
185+
} catch {
186+
return false;
187+
}
188+
};
189+
170190
return (
171191
<QueryClientProvider client={queryClient}>
172192
<PrivyProvider
@@ -184,6 +204,10 @@ export default function AppWithProviders() {
184204
<GestureHandlerRootView style={{ flex: 1 }}>
185205
<BottomSheetModalProvider>
186206
<App />
207+
{/* <AuthenticateWithPasskeyProvider>
208+
<PrivyPlaygroundLandingScreen />
209+
</AuthenticateWithPasskeyProvider> */}
210+
{__DEV__ && <DevToolsBubble onCopy={onCopy} />}
187211
<Snackbars />
188212
</BottomSheetModalProvider>
189213
</GestureHandlerRootView>

0 commit comments

Comments
 (0)