Skip to content

Commit 0098d10

Browse files
authored
Ml/link web3 with xmtp profile (#1617)
1 parent 4fb5fd8 commit 0098d10

35 files changed

+535
-619
lines changed

App.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import { preventSplashScreenAutoHide } from "./utils/splash/splash";
4343
import { setupStreamingSubscriptions } from "@/features/streams/streams";
4444
import { useInitializeMultiInboxClient } from "@/features/multi-inbox/multi-inbox.client";
4545
import logger from "./utils/logger";
46-
import { AuthenticateWithPasskeyProvider } from "@/features/onboarding/contexts/signup-with-passkey.context";
46+
import { AuthenticateWithPasskeyProvider } from "@/features/authentication/authenticate-with-passkey.context";
4747
import { PrivyPlaygroundLandingScreen } from "@/features/privy-playground/privy-playground-landing.screen";
4848
import { useLogoutOnJwtRefreshError } from "@/features/authentication/use-logout-on-jwt-refresh-error";
4949
import { useMonitorNetworkConnectivity } from "./dependencies/NetworkMonitor/use-monitor-network-connectivity";

SlimApp.tsx

+10-5
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ import { useCoinbaseWalletListener } from "@utils/coinbaseWallet";
1212
import { converseEventEmitter } from "@utils/events";
1313
import "expo-dev-client";
1414
import React, { useCallback, useEffect, useMemo, useRef } from "react";
15-
import { Text, useColorScheme } from "react-native";
15+
import { SafeAreaView, Text, useColorScheme } from "react-native";
1616
import { GestureHandlerRootView } from "react-native-gesture-handler";
1717
import { KeyboardProvider } from "react-native-keyboard-controller";
1818
import { Provider as PaperProvider } from "react-native-paper";
1919
import { config } from "@/config";
2020
import { useInitializeMultiInboxClient } from "./features/multi-inbox/multi-inbox.client";
2121
import { PrivyProvider } from "@privy-io/expo";
2222
import { ThirdwebProvider } from "thirdweb/react";
23-
import { AuthenticateWithPasskeyProvider } from "./features/onboarding/contexts/signup-with-passkey.context";
23+
import { AuthenticateWithPasskeyProvider } from "./features/authentication/authenticate-with-passkey.context";
2424
import { PrivyPlaygroundLandingScreen } from "./features/privy-playground/privy-playground-landing.screen";
2525
import { DevToolsBubble } from "react-native-react-query-devtools";
2626
import * as Clipboard from "expo-clipboard";
@@ -37,6 +37,9 @@ export function SlimApp() {
3737
useInitializeMultiInboxClient();
3838
useReactQueryDevTools(queryClient);
3939
useMonitorNetworkConnectivity();
40+
useEffect(() => {
41+
setupAppAttest();
42+
}, []);
4043

4144
const { themeScheme, setThemeContextOverride, ThemeProvider } =
4245
useThemeProvider();
@@ -63,9 +66,11 @@ export function SlimApp() {
6366
<PaperProvider theme={paperTheme}>
6467
<GestureHandlerRootView style={{ flex: 1 }}>
6568
<BottomSheetModalProvider>
66-
<AuthenticateWithPasskeyProvider>
67-
<PrivyPlaygroundLandingScreen />
68-
</AuthenticateWithPasskeyProvider>
69+
<SafeAreaView style={{ flex: 1 }}>
70+
<AuthenticateWithPasskeyProvider>
71+
<PrivyPlaygroundLandingScreen />
72+
</AuthenticateWithPasskeyProvider>
73+
</SafeAreaView>
6974
{__DEV__ && <DevToolsBubble onCopy={onCopy} />}
7075
<Snackbars />
7176
</BottomSheetModalProvider>

components/DebugButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import * as Updates from "expo-updates";
1414
import { forwardRef, useImperativeHandle } from "react";
1515
import { Alert, Platform } from "react-native";
1616
import { showActionSheetWithOptions } from "./StateHandlers/ActionSheetStateHandler";
17-
import { useLogout } from "@/utils/logout";
17+
import { useLogout } from "@/features/authentication/use-logout.hook";
1818

1919
export const DebugButton = forwardRef((props, ref) => {
2020
const { logout } = useLogout();

config/development.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,9 @@ export const developmentConfig: IConfig = {
1414
),
1515
appCheckDebugToken:
1616
Platform.OS === "android"
17-
? // @ts-expect-error todo fixme
18-
process.env.EXPO_PUBLIC_FIREBASE_APP_CHECK_DEBUG_TOKEN_ANDROID
19-
: // @ts-expect-error todo fixme
20-
process.env.EXPO_PUBLIC_FIREBASE_APP_CHECK_DEBUG_TOKEN_IOS,
17+
? process.env.EXPO_PUBLIC_FIREBASE_APP_CHECK_DEBUG_TOKEN_ANDROID
18+
: process.env.EXPO_PUBLIC_FIREBASE_APP_CHECK_DEBUG_TOKEN_IOS,
2119
evm: {
22-
// @ts-expect-error todo fixme
2320
rpcEndpoint: process.env.EXPO_PUBLIC_EVM_RPC_ENDPOINT,
2421
transactionChainId: "0x14a34", // Base Sepolia
2522
USDC: {
@@ -29,4 +26,6 @@ export const developmentConfig: IConfig = {
2926
decimals: 6,
3027
},
3128
},
29+
reactQueryEncryptionKey:
30+
process.env.EXPO_PUBLIC_SECURE_REACT_QUERY_ENCRYPTION_KEY || "",
3231
} as const;

features/onboarding/contexts/signup-with-passkey.context.tsx features/authentication/authenticate-with-passkey.context.tsx

+14-13
Original file line numberDiff line numberDiff line change
@@ -174,22 +174,23 @@ export const AuthenticateWithPasskeyProvider = ({
174174
"[passkey onboarding context] smart contract wallet address",
175175
smartContractWalletAddress
176176
);
177-
const user = await createUser({
178-
privyUserId: privyUser!.id,
179-
smartContractWalletAddress,
180-
inboxId,
181-
profile: {
182-
name: `Test User ${Math.random()}`,
183-
// avatar: privyUser!.imageUrl,
184-
},
185-
});
186-
logger.debug(
187-
"[passkey onboarding context] created user",
188-
JSON.stringify(user, null, 2)
189-
);
177+
// const user = await createUser({
178+
// privyUserId: privyUser!.id,
179+
// smartContractWalletAddress,
180+
// inboxId,
181+
// profile: {
182+
// name: `Test User ${Math.random()}`,
183+
// // avatar: privyUser!.imageUrl,
184+
// },
185+
// });
186+
// logger.debug(
187+
// "[passkey onboarding context] created user",
188+
// JSON.stringify(user, null, 2)
189+
// );
190190
logger.debug(
191191
"[passkey onboarding context] signing up and created a new inbox successfully in useXmtpFromPrivySmartWalletClientSigner"
192192
);
193+
useAccountsStore.getState().setAuthStatus(AuthStatuses.signingUp);
193194
} catch (error) {
194195
logger.error(
195196
"[passkey onboarding context] Error creating user:",
+41-25
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
import { buildDeviceMetadata } from "@/utils/device-metadata";
22
import { api } from "@/utils/api/api";
33
import { z } from "zod";
4+
import { logger } from "@/utils/logger";
45

56
const deviceOSEnum = z.enum(["android", "ios", "web"]);
67

7-
const createUserResponseSchema = z.object({
8-
id: z.string(),
9-
privyUserId: z.string(),
10-
device: z.object({
8+
const createUserResponseSchema = z
9+
.object({
1110
id: z.string(),
12-
os: deviceOSEnum,
13-
name: z.string().nullable(),
14-
}),
15-
identity: z.object({
16-
id: z.string(),
17-
privyAddress: z.string(),
18-
xmtpId: z.string(),
19-
}),
20-
profile: z.object({
21-
id: z.string(),
22-
name: z.string(),
23-
description: z.string().nullable(),
24-
}),
25-
});
11+
privyUserId: z.string(),
12+
device: z.object({
13+
id: z.string(),
14+
os: deviceOSEnum,
15+
name: z.string().nullable(),
16+
}),
17+
identity: z.object({
18+
id: z.string(),
19+
privyAddress: z.string(),
20+
xmtpId: z.string(),
21+
}),
22+
profile: z.object({
23+
id: z.string(),
24+
name: z.string(),
25+
description: z.string().nullable(),
26+
}),
27+
})
28+
.strict();
2629

2730
export type CreateUserResponse = z.infer<typeof createUserResponseSchema>;
2831

@@ -51,20 +54,33 @@ export const createUser = async (args: {
5154
"/api/v1/users",
5255
requestData
5356
);
57+
logger.debug(
58+
`[createUser] Response from /api/v1/users: ${JSON.stringify(
59+
response.data,
60+
null,
61+
2
62+
)}`
63+
);
5464
return createUserResponseSchema.parse(response.data);
5565
};
5666

5767
const fetchJwtResponseSchema = z.object({
58-
jwt: z.string(),
68+
token: z.string(),
5969
});
6070

6171
type FetchJwtResponse = z.infer<typeof fetchJwtResponseSchema>;
6272

6373
export async function fetchJwt() {
64-
// todo(lustig) look at the endpoint this actually is
65-
// const response = await api.post<FetchJwtResponse>("/api/v1/authenticate");
66-
// return fetchJwtResponseSchema.parse(response.data);
67-
// https://xmtp-labs.slack.com/archives/C07NSHXK693/p1739877738959529
68-
const dummyJwtUntilJwtBackendWorks = "dummyJwtUntilJwtBackendWorks";
69-
return dummyJwtUntilJwtBackendWorks;
74+
logger.debug(`[fetchJwt] Fetching JWT token`);
75+
const response = await api.post<FetchJwtResponse>("/api/v1/authenticate");
76+
logger.debug(
77+
`[fetchJwt] Response from /api/v1/authenticate: ${JSON.stringify(
78+
response.data,
79+
null,
80+
2
81+
)}`
82+
);
83+
const parsedResponse = fetchJwtResponseSchema.parse(response.data);
84+
logger.debug(`[fetchJwt] Successfully fetched JWT token`);
85+
return parsedResponse;
7086
}

features/authentication/authentication.headers.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import { toHex } from "viem";
99
import { ensureJwtQueryData } from "./jwt.query";
1010
import { AuthenticationError } from "@/utils/error";
1111

12+
// used for requests that are creating an authentication token
1213
export const XMTP_INSTALLATION_ID_HEADER_KEY = "X-XMTP-InstallationId";
1314
export const XMTP_INBOX_ID_HEADER_KEY = "X-XMTP-InboxId";
1415
export const FIREBASE_APP_CHECK_HEADER_KEY = "X-Firebase-AppCheck";
1516
export const XMTP_SIGNATURE_HEADER_KEY = "X-XMTP-Signature";
17+
18+
// used for authenticated requests
1619
export const CONVOS_AUTH_TOKEN_HEADER_KEY = "X-Convos-AuthToken";
1720

1821
export type XmtpApiHeaders = {
@@ -34,39 +37,67 @@ export type XmtpApiHeaders = {
3437
* Note: We don't use refresh tokens since users are already authenticated via
3538
* Privy passkeys. See authentication.readme.md for more details.
3639
*/
40+
import { logger } from "@/utils/logger";
41+
3742
export async function getConvosAuthenticationHeaders(): Promise<XmtpApiHeaders> {
43+
logger.debug(
44+
"[getConvosAuthenticationHeaders] Starting to get authentication headers"
45+
);
46+
3847
const currentEthereumAddress = getSafeCurrentSender().ethereumAddress;
48+
logger.debug(
49+
`[getConvosAuthenticationHeaders] Current ethereum address: ${currentEthereumAddress}`
50+
);
51+
3952
const areInboxesRestored =
4053
useAccountsStore.getState().multiInboxClientRestorationState ===
41-
MultiInboxClientRestorationStates.idle;
54+
MultiInboxClientRestorationStates.restored;
55+
logger.debug(
56+
`[getConvosAuthenticationHeaders] Are inboxes restored: ${
57+
useAccountsStore.getState().multiInboxClientRestorationState
58+
}`
59+
);
4260
const appCheckToken = await tryGetAppCheckToken();
4361
const inboxClient = MultiInboxClient.instance.getInboxClientForAddress({
4462
ethereumAddress: currentEthereumAddress,
4563
});
4664

4765
if (!appCheckToken) {
66+
logger.error(
67+
"[getConvosAuthenticationHeaders] No App Check Token Available"
68+
);
4869
throw new AuthenticationError(
4970
"No App Check Token Available. This indicates that we believe the app is not running on an authentic build of our application on a device that has not been tampered with."
5071
);
5172
}
5273

5374
if (!areInboxesRestored) {
75+
logger.error("[getConvosAuthenticationHeaders] Inboxes not restored");
5476
throw new AuthenticationError(
5577
"[getConvosAuthenticationHeaders] Inboxes not restored; cannot create headers"
5678
);
5779
}
5880

5981
if (!inboxClient) {
82+
logger.error(
83+
"[getConvosAuthenticationHeaders] No inbox client found for account"
84+
);
6085
throw new AuthenticationError(
6186
"[getConvosAuthenticationHeaders] No inbox client found for account"
6287
);
6388
}
6489

90+
logger.debug(
91+
"[getConvosAuthenticationHeaders] Signing app check token with installation key"
92+
);
6593
const rawAppCheckTokenSignature = await inboxClient.signWithInstallationKey(
6694
appCheckToken
6795
);
6896
const appCheckTokenSignatureHexString = toHex(rawAppCheckTokenSignature);
6997

98+
logger.debug(
99+
"[getConvosAuthenticationHeaders] Successfully created authentication headers"
100+
);
70101
return {
71102
[XMTP_INSTALLATION_ID_HEADER_KEY]: inboxClient.installationId,
72103
[XMTP_INBOX_ID_HEADER_KEY]: inboxClient.inboxId,

features/authentication/interceptor.headers.ts

+24-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getConvosAuthenticatedHeaders,
55
} from "@/features/authentication/authentication.headers";
66
import { AuthenticationError } from "../../utils/error";
7+
import { logger } from "@/utils/logger";
78

89
/**
910
* These routes are special because they need to be called before we have a JWT.
@@ -19,26 +20,41 @@ import { AuthenticationError } from "../../utils/error";
1920
* See authentication.readme.md for more details on the authentication flow
2021
* and how these headers are used to establish trust before issuing a JWT.
2122
*/
22-
const AuthenticationRoutes = ["/api/v1/users", "/api/v1/authenticate"] as const;
23+
const AuthenticationRoutes = ["/api/v1/authenticate"] as const;
2324

2425
export const headersInterceptor = async (config: AxiosRequestConfig) => {
2526
const url = config.url;
2627

2728
if (!url) {
29+
logger.error("[headersInterceptor] No URL provided in request config");
2830
throw new AuthenticationError("No URL provided in request config");
2931
}
3032

33+
logger.debug(`[headersInterceptor] Processing request for URL: ${url}`);
34+
3135
const needsAuthenticationHeaders = AuthenticationRoutes.some((route) =>
3236
url.includes(route)
3337
);
38+
logger.debug(
39+
`[headersInterceptor] Needs authentication headers: ${needsAuthenticationHeaders}`
40+
);
41+
let headers;
42+
try {
43+
headers = needsAuthenticationHeaders
44+
? await getConvosAuthenticationHeaders()
45+
: await getConvosAuthenticatedHeaders();
3446

35-
const isJwtSetupOnBackend = /** not prioritized for March launch */ false;
36-
37-
const shouldUseJwtHeaders = isJwtSetupOnBackend && needsAuthenticationHeaders;
38-
39-
const headers = shouldUseJwtHeaders
40-
? await getConvosAuthenticationHeaders()
41-
: await getConvosAuthenticatedHeaders();
47+
logger.debug(
48+
`[headersInterceptor] Headers generated successfully, ${JSON.stringify(
49+
headers,
50+
null,
51+
2
52+
)}`
53+
);
54+
} catch (error) {
55+
logger.error(`[headersInterceptor] Failed to generate headers: ${error}`);
56+
throw error;
57+
}
4258

4359
config.headers = {
4460
...config.headers,

0 commit comments

Comments
 (0)