diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 367a3cc6b..761cf0f86 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -91,6 +91,8 @@ + + diff --git a/android/app/src/preview/AndroidManifest.xml b/android/app/src/preview/AndroidManifest.xml index 5ef15e678..1a29663fc 100644 --- a/android/app/src/preview/AndroidManifest.xml +++ b/android/app/src/preview/AndroidManifest.xml @@ -23,6 +23,8 @@ + + diff --git a/android/app/src/prod/AndroidManifest.xml b/android/app/src/prod/AndroidManifest.xml index f0ee2c224..12a518196 100644 --- a/android/app/src/prod/AndroidManifest.xml +++ b/android/app/src/prod/AndroidManifest.xml @@ -23,6 +23,8 @@ + + diff --git a/components/Onboarding/ConnectViaWallet/ConnectViaWallet.store.tsx b/components/Onboarding/ConnectViaWallet/ConnectViaWallet.store.tsx index 532ec7cc9..0e0853b98 100644 --- a/components/Onboarding/ConnectViaWallet/ConnectViaWallet.store.tsx +++ b/components/Onboarding/ConnectViaWallet/ConnectViaWallet.store.tsx @@ -6,6 +6,7 @@ type IConnectViaWalletStoreProps = { address: string; alreadyV3Db: boolean; signer: Signer; + isSCW: boolean; }; type IConnectViaWalletStoreProviderProps = @@ -42,6 +43,7 @@ const createConnectViaWalletStore = (props: IConnectViaWalletStoreProps) => createStore()((set) => ({ address: props.address, signer: props.signer, + isSCW: props.isSCW, alreadyV3Db: props.alreadyV3Db, loading: false, numberOfSignaturesDone: 0, diff --git a/components/Onboarding/ConnectViaWallet/ConnectViaWallet.tsx b/components/Onboarding/ConnectViaWallet/ConnectViaWallet.tsx index ab659d789..fce072771 100644 --- a/components/Onboarding/ConnectViaWallet/ConnectViaWallet.tsx +++ b/components/Onboarding/ConnectViaWallet/ConnectViaWallet.tsx @@ -24,6 +24,7 @@ import { useInitConnectViaWalletState } from "./useInitConnectViaWalletState"; type IConnectViaWalletProps = { address: string; + isSCW: boolean; onDoneConnecting: () => void; onErrorConnecting: (arg: { error: Error }) => void; }; @@ -31,7 +32,7 @@ type IConnectViaWalletProps = { export const ConnectViaWallet = memo(function ConnectViaWallet( props: IConnectViaWalletProps ) { - const { address, onErrorConnecting, onDoneConnecting } = props; + const { address, isSCW, onErrorConnecting, onDoneConnecting } = props; const finishedConnectingRef = useRef(false); @@ -71,16 +72,22 @@ export const ConnectViaWallet = memo(function ConnectViaWallet( onDoneConnecting={handleDoneConnecting} onErrorConnecting={handleErrorConnecting} > - + ); }); // Wrapper to init the wallet state and then provide the data to the UI const ConnectViaWalletStateWrapper = memo( - function ConnectViaWalletStateWrapper({ address }: { address: string }) { + function ConnectViaWalletStateWrapper({ + address, + isSCW, + }: { + address: string; + isSCW: boolean; + }) { const { isInitializing, alreadyV3Db, signer } = - useInitConnectViaWalletState({ address }); + useInitConnectViaWalletState({ address, isSCW }); if (isInitializing) { return ; @@ -94,6 +101,7 @@ const ConnectViaWalletStateWrapper = memo( return ( diff --git a/components/Onboarding/ConnectViaWallet/ConnectViaWalletSupportedWallets.ts b/components/Onboarding/ConnectViaWallet/ConnectViaWalletSupportedWallets.ts index 3d19cd2b8..acaa47710 100644 --- a/components/Onboarding/ConnectViaWallet/ConnectViaWalletSupportedWallets.ts +++ b/components/Onboarding/ConnectViaWallet/ConnectViaWalletSupportedWallets.ts @@ -16,6 +16,7 @@ import { WalletId } from "thirdweb/wallets"; import { useAppStateHandlers } from "../../../hooks/useAppStateHandlers"; import { isEthOS } from "../../../utils/ethos"; +import { InstalledWallet, SUPPORTED_WALLETS } from "@utils/evm/wallets"; export const POPULAR_WALLETS = [ { @@ -56,116 +57,6 @@ export const POPULAR_WALLETS = [ }, ]; -const SUPPORTED_WALLETS = [ - { - name: "Coinbase Wallet", - iconURL: - "https://explorer-api.walletconnect.com/v3/logo/sm/a5ebc364-8f91-4200-fcc6-be81310a0000?projectId=2f05ae7f1116030fde2d36508f472bfb", - customScheme: "cbwallet://", - thirdwebId: "com.coinbase.wallet", - }, - { - name: "Ledger Live", - walletConnectId: - "19177a98252e07ddfc9af2083ba8e07ef627cb6103467ffebb3f8f4205fd7927", - iconURL: - "https://explorer-api.walletconnect.com/v3/logo/sm/a7f416de-aa03-4c5e-3280-ab49269aef00?projectId=2f05ae7f1116030fde2d36508f472bfb", - customScheme: "ledgerlive://", - thirdwebId: "com.ledger", - }, - { - name: "Rainbow", - walletConnectId: - "1ae92b26df02f0abca6304df07debccd18262fdf5fe82daa81593582dac9a369", - iconURL: - "https://explorer-api.walletconnect.com/v3/logo/sm/7a33d7f1-3d12-4b5c-f3ee-5cd83cb1b500?projectId=2f05ae7f1116030fde2d36508f472bfb", - customScheme: "rainbow://", - thirdwebId: "me.rainbow", - // Rainbow Mobile does not support tesnets (even Sepolia) - // https://rainbow.me/en/support/app/testnets - supportedChains: [ - ethereum, - base, - optimism, - polygon, - arbitrum, - avalanche, - blast, - zora, - ], - }, - { - name: "MetaMask", - walletConnectId: - "c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96", - iconURL: - "https://explorer-api.walletconnect.com/v3/logo/sm/018b2d52-10e9-4158-1fde-a5d5bac5aa00?projectId=2f05ae7f1116030fde2d36508f472bfb", - customScheme: "metamask://", - thirdwebId: "io.metamask", - }, - { - name: "Trust Wallet", - walletConnectId: - "4622a2b2d6af1c9844944291e5e7351a6aa24cd7b23099efac1b2fd875da31a0", - iconURL: - "https://explorer-api.walletconnect.com/v3/logo/sm/0528ee7e-16d1-4089-21e3-bbfb41933100?projectId=2f05ae7f1116030fde2d36508f472bfb", - customScheme: "trust://", - thirdwebId: "com.trustwallet.app", - }, - { - name: "Uniswap Wallet", - walletConnectId: - "c03dfee351b6fcc421b4494ea33b9d4b92a984f87aa76d1663bb28705e95034a", - iconURL: - "https://explorer-api.walletconnect.com/v3/logo/sm/bff9cf1f-df19-42ce-f62a-87f04df13c00?projectId=2f05ae7f1116030fde2d36508f472bfb", - customScheme: "uniswap://", - thirdwebId: "org.uniswap", - }, - { - name: "Zerion", - walletConnectId: - "ecc4036f814562b41a5268adc86270fba1365471402006302e70169465b7ac18", - iconURL: - "https://explorer-api.walletconnect.com/v3/logo/sm/73f6f52f-7862-49e7-bb85-ba93ab72cc00?projectId=2f05ae7f1116030fde2d36508f472bfb", - customScheme: "zerion://", - thirdwebId: "io.zerion.wallet", - }, - { - name: "Exodus", - walletConnectId: - "e9ff15be73584489ca4a66f64d32c4537711797e30b6660dbcb71ea72a42b1f4", - iconURL: - "https://explorer-api.walletconnect.com/v3/logo/sm/4c16cad4-cac9-4643-6726-c696efaf5200?projectId=2f05ae7f1116030fde2d36508f472bfb", - customScheme: "exodus://", - universalLink: "https://exodus.com/m", - thirdwebId: "com.exodus", - }, - // { - // name: "1inch Wallet", - // walletConnectId: - // "c286eebc742a537cd1d6818363e9dc53b21759a1e8e5d9b263d0c03ec7703576", - // iconURL: - // "https://explorer-api.walletconnect.com/v3/logo/sm/52b1da3c-9e72-40ae-5dac-6142addd9c00?projectId=2f05ae7f1116030fde2d36508f472bfb", - // customScheme: "oneinch://", - // universalLink: "https://wallet.1inch.io", - // }, -]; - -type ISupportedWalletName = - | (typeof SUPPORTED_WALLETS)[number]["name"] - | "EthOS Wallet"; - -export type InstalledWallet = { - name: ISupportedWalletName; - iconURL: string; - customScheme?: string; - universalLink?: string; - walletConnectId?: string; - platforms?: string[]; - thirdwebId?: WalletId; - supportedChains?: Chain[]; -}; - let hasCheckedInstalled = false; export let installedWallets: InstalledWallet[] = []; @@ -174,7 +65,9 @@ export const getInstalledWallets = async ( ): Promise => { if (hasCheckedInstalled && !refresh) return installedWallets; const checkInstalled = await Promise.all( - SUPPORTED_WALLETS.map((w) => Linking.canOpenURL(`${w.customScheme}wc`)) + Object.values(SUPPORTED_WALLETS).map( + (w) => !!w.customScheme && Linking.canOpenURL(`${w.customScheme}wc`) + ) ); const wallets: InstalledWallet[] = []; @@ -187,8 +80,8 @@ export const getInstalledWallets = async ( } wallets.push( - ...(SUPPORTED_WALLETS as InstalledWallet[]).filter( - (w, i) => checkInstalled[i] + ...(Object.values(SUPPORTED_WALLETS) as InstalledWallet[]).filter( + (w, i) => checkInstalled[i] || w.isSmartContractWallet ) ); installedWallets = wallets; diff --git a/components/Onboarding/ConnectViaWallet/ConnectViaWalletTableViewItems.tsx b/components/Onboarding/ConnectViaWallet/ConnectViaWalletTableViewItems.tsx index 70795071f..d9706ae3c 100644 --- a/components/Onboarding/ConnectViaWallet/ConnectViaWalletTableViewItems.tsx +++ b/components/Onboarding/ConnectViaWallet/ConnectViaWalletTableViewItems.tsx @@ -1,5 +1,6 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; // TODO: move out of ConnectViaWallet -import { memo, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState } from "react"; import { ActivityIndicator } from "react-native"; import { useDisconnect as useThirdwebDisconnect, @@ -7,22 +8,19 @@ import { useConnect as useThirdwebConnect, useActiveWallet as useThirdwebActiveWallet, } from "thirdweb/react"; -import { createWallet } from "thirdweb/wallets"; -import { - InstalledWallet, - useInstalledWallets, -} from "./ConnectViaWalletSupportedWallets"; +import { useInstalledWallets } from "./ConnectViaWalletSupportedWallets"; import config from "../../../config"; import { getAccountsList } from "../../../data/store/accountsStore"; import { useAppStateHandlers } from "../../../hooks/useAppStateHandlers"; import { translate } from "../../../i18n"; import { getEthOSSigner } from "../../../utils/ethos"; import logger from "../../../utils/logger"; -import { thirdwebClient } from "../../../utils/thirdweb"; +import { thirdwebClient, thirdwebWallets } from "../../../utils/thirdweb"; import TableView, { TableViewItemType } from "../../TableView/TableView"; import { TableViewEmoji, TableViewImage } from "../../TableView/TableViewImage"; import { RightViewChevron } from "../../TableView/TableViewRightChevron"; +import { InstalledWallet, ISupportedWalletName } from "@utils/evm/wallets"; export function getConnectViaWalletTableViewPrivateKeyItem( args: Partial @@ -79,7 +77,7 @@ export function getConnectViaWalletInstalledWalletTableViewItem(args: { export const InstalledWalletsTableView = memo( function InstalledWalletsTableView(props: { onAccountExists: (arg: { address: string }) => void; - onAccountDoesNotExist: (arg: { address: string }) => void; + onAccountDoesNotExist: (arg: { address: string; isSCW: boolean }) => void; }) { const { onAccountExists, onAccountDoesNotExist } = props; @@ -87,9 +85,24 @@ export const InstalledWalletsTableView = memo( const { connect: thirdwebConnect } = useThirdwebConnect(); const { disconnect: disconnectThirdweb } = useThirdwebDisconnect(); + const thirdwebActiveWallet = useThirdwebActiveWallet(); + const thirdwebActiveWalletRef = useRef(thirdwebActiveWallet); + useEffect(() => { + thirdwebActiveWalletRef.current = thirdwebActiveWallet; + }, [thirdwebActiveWallet]); + const setThirdwebActiveWallet = useSetThirdwebActiveWallet(); + const disconnectActiveThirdweb = useCallback(async () => { + if (!thirdwebActiveWalletRef.current) return; + disconnectThirdweb(thirdwebActiveWalletRef.current); + // Wait for the disconnect to complete + while (!!thirdwebActiveWalletRef.current) { + await new Promise((r) => setTimeout(r, 100)); + } + }, [disconnectThirdweb]); + const [isProcessingWalletId, setIsProcessingWalletId] = useState< string | null >(null); @@ -117,38 +130,35 @@ export const InstalledWalletsTableView = memo( walletName: wallet.name, }), action: async () => { + const isSCW = !!wallet?.isSmartContractWallet; logger.debug( - `[Onboarding] Clicked on wallet ${wallet.name} - opening external app` + `[Onboarding] Clicked on wallet ${wallet.name} - ${ + isSCW ? "Opening web page" : "opening external app" + }` ); setIsProcessingWalletId(wallet.name); - if (thirdwebActiveWallet) { - disconnectThirdweb(thirdwebActiveWallet); - } + await disconnectActiveThirdweb(); try { let walletAddress: string = ""; // Specific flow for Coinbase Wallet - if (wallet.name === "Coinbase Wallet") { - const wallet = await thirdwebConnect(async () => { - const coinbaseWallet = createWallet("com.coinbase.wallet", { - appMetadata: config.walletConnectConfig.appMetadata, - mobileConfig: { - callbackURL: `https://${config.websiteDomain}/coinbase`, - }, - }); + if (wallet.thirdwebId === "com.coinbase.wallet") { + const thirdwebWallet = await thirdwebConnect(async () => { + const coinbaseWallet = + thirdwebWallets[wallet.name as ISupportedWalletName]; await coinbaseWallet.connect({ client: thirdwebClient }); setThirdwebActiveWallet(coinbaseWallet); return coinbaseWallet; }); - if (!wallet) { + if (!thirdwebWallet) { throw new Error("No coinbase wallet"); } - const account = wallet.getAccount(); + const account = thirdwebWallet.getAccount(); if (!account) { throw new Error("No coinbase account found"); @@ -166,7 +176,8 @@ export const InstalledWalletsTableView = memo( } // Generic flow for all other wallets else if (wallet.thirdwebId) { - const walletConnectWallet = createWallet(wallet.thirdwebId); + const walletConnectWallet = + thirdwebWallets[wallet.name as ISupportedWalletName]; const account = await walletConnectWallet.connect({ client: thirdwebClient, walletConnect: config.walletConnectConfig, @@ -179,7 +190,10 @@ export const InstalledWalletsTableView = memo( if (getAccountsList().includes(walletAddress)) { onAccountExists({ address: walletAddress }); } else { - onAccountDoesNotExist({ address: walletAddress }); + onAccountDoesNotExist({ + address: walletAddress, + isSCW, + }); } } catch (e: any) { logger.error("Error connecting to wallet:", e); diff --git a/components/Onboarding/ConnectViaWallet/useConnectViaWalletInitXmtpClient.tsx b/components/Onboarding/ConnectViaWallet/useConnectViaWalletInitXmtpClient.tsx index f4df8a0a2..914a91eec 100644 --- a/components/Onboarding/ConnectViaWallet/useConnectViaWalletInitXmtpClient.tsx +++ b/components/Onboarding/ConnectViaWallet/useConnectViaWalletInitXmtpClient.tsx @@ -53,6 +53,7 @@ export function useInitXmptClient() { const signer = connectViewWalletStore.getState().signer!; // We can assume that signer is set at this point const address = connectViewWalletStore.getState().address!; // We can assume that address is set at this point const alreadyV3Db = connectViewWalletStore.getState().alreadyV3Db; + const isSCW = connectViewWalletStore.getState().isSCW; const waitForClickSignature = async () => { while (!connectViewWalletStore.getState().clickedSignature) { @@ -70,6 +71,7 @@ export function useInitXmptClient() { await createXmtpClientFromSigner( signer, + isSCW, async () => { logger.debug("[Connect Wallet] Installation revoked, disconnecting"); try { diff --git a/components/Onboarding/ConnectViaWallet/useInitConnectViaWalletState.ts b/components/Onboarding/ConnectViaWallet/useInitConnectViaWalletState.ts index 16c700156..9353721a7 100644 --- a/components/Onboarding/ConnectViaWallet/useInitConnectViaWalletState.ts +++ b/components/Onboarding/ConnectViaWallet/useInitConnectViaWalletState.ts @@ -19,8 +19,11 @@ import { getInboxId } from "@utils/xmtpRN/signIn"; * But ideally a lot of this it outside React. * This function is for initializing the state of the wallet. V3? XMTP? etc... */ -export function useInitConnectViaWalletState(args: { address: string }) { - const { address } = args; +export function useInitConnectViaWalletState(args: { + address: string; + isSCW: boolean; +}) { + const { address, isSCW } = args; const { onErrorConnecting } = useConnectViaWalletContext(); @@ -91,7 +94,9 @@ export function useInitConnectViaWalletState(args: { address: string }) { setSigner(thirdwebSigner); logger.debug( - `[Connect Wallet] User connected wallet ${thirdwebWallet?.id} (${address}). V3 database ${hasV3 ? "already" : "not"} present` + `[Connect Wallet] User connected ${ + isSCW ? "SCW" : "EOA" + } wallet ${thirdwebWallet?.id} (${address}). V3 database ${hasV3 ? "already" : "not"} present` ); } catch (error) { logger.error("[Connect Wallet] Error initializing wallet:", error); @@ -109,6 +114,7 @@ export function useInitConnectViaWalletState(args: { address: string }) { setIsInitializing, thirdwebWallet, thirdwebSigner, + isSCW, onErrorConnecting, ]); diff --git a/components/Onboarding/init-xmtp-client.ts b/components/Onboarding/init-xmtp-client.ts index ec41fbb51..f1afbd8a9 100644 --- a/components/Onboarding/init-xmtp-client.ts +++ b/components/Onboarding/init-xmtp-client.ts @@ -30,7 +30,7 @@ export async function initXmtpClient(args: { } try { - await createXmtpClientFromSigner(signer, async () => { + await createXmtpClientFromSigner(signer, false, async () => { await awaitableAlert( translate("current_installation_revoked"), translate("current_installation_revoked_description") diff --git a/components/TransactionPreview/TransactionActions.tsx b/components/TransactionPreview/TransactionActions.tsx index dfcddab56..0e0f276c5 100644 --- a/components/TransactionPreview/TransactionActions.tsx +++ b/components/TransactionPreview/TransactionActions.tsx @@ -1,4 +1,3 @@ -import { InstalledWallet } from "@components/Onboarding/ConnectViaWallet/ConnectViaWalletSupportedWallets"; import { Text } from "@design-system/Text"; import { TouchableOpacity } from "@design-system/TouchableOpacity"; import { VStack } from "@design-system/VStack"; @@ -7,7 +6,7 @@ import { spacing } from "@theme/spacing"; import { TransactionToTrigger } from "./TransactionPreview"; import TransactionNext from "../../assets/transaction-next.svg"; -import { CHAIN_BY_ID } from "@utils/evm/wallets"; +import { CHAIN_BY_ID, InstalledWallet } from "@utils/evm/wallets"; type ITransactionActionsProps = { shouldSwitchChain?: boolean; diff --git a/components/TransactionPreview/TransactionContent.tsx b/components/TransactionPreview/TransactionContent.tsx index 08d93ef62..9ae224f4a 100644 --- a/components/TransactionPreview/TransactionContent.tsx +++ b/components/TransactionPreview/TransactionContent.tsx @@ -1,4 +1,3 @@ -import { InstalledWallet } from "@components/Onboarding/ConnectViaWallet/ConnectViaWalletSupportedWallets"; import { translate } from "@i18n"; import { SimulateAssetChangesResponse } from "alchemy-sdk"; @@ -10,6 +9,7 @@ import { TransactionPreviewRow } from "./TransactionPreviewRow"; import { SimulationResult } from "./TransactionSimulationResult"; import { TransactionResult } from "./TransactionResult"; import { shortAddress } from "@utils/strings/shortAddress"; +import { InstalledWallet } from "@/utils/evm/wallets"; type TransactionState = { status: "pending" | "triggering" | "triggered" | "success" | "failure"; diff --git a/features/ExternalWalletPicker/ExternalWalletPicker.tsx b/features/ExternalWalletPicker/ExternalWalletPicker.tsx index 81de8dc6e..7640419ab 100644 --- a/features/ExternalWalletPicker/ExternalWalletPicker.tsx +++ b/features/ExternalWalletPicker/ExternalWalletPicker.tsx @@ -11,19 +11,17 @@ import { $globalStyles } from "@theme/styles"; import { ThemedStyle, useAppTheme } from "@theme/useAppTheme"; import { waitUntilAppActive } from "@utils/appState/waitUntilAppActive"; import { ensureError } from "@utils/error"; -import { thirdwebClient } from "@utils/thirdweb"; +import { thirdwebClient, thirdwebWallets } from "@utils/thirdweb"; import { Image } from "expo-image"; import { useCallback } from "react"; import { Alert, ViewStyle } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { Account, createWallet, Wallet } from "thirdweb/wallets"; +import { Account, Wallet } from "thirdweb/wallets"; import { useExternalWalletPickerContext } from "./ExternalWalletPicker.context"; -import { - InstalledWallet, - useInstalledWallets, -} from "../../components/Onboarding/ConnectViaWallet/ConnectViaWalletSupportedWallets"; +import { useInstalledWallets } from "../../components/Onboarding/ConnectViaWallet/ConnectViaWalletSupportedWallets"; import config from "../../config"; +import { InstalledWallet, ISupportedWalletName } from "@utils/evm/wallets"; type IExternalWalletPickerProps = { title?: string; @@ -50,20 +48,10 @@ export function ExternalWalletPicker(props: IExternalWalletPickerProps) { throw new Error("No installed wallet could be found"); } - let wallet: Wallet | undefined; + const wallet = + thirdwebWallets[installedWallet.name as ISupportedWalletName]; let account: Account | undefined; - if (installedWallet.name === "Coinbase Wallet") { - wallet = createWallet("com.coinbase.wallet", { - appMetadata: config.walletConnectConfig.appMetadata, - mobileConfig: { - callbackURL: `https://${config.websiteDomain}/coinbase`, - }, - }); - } else if (installedWallet.thirdwebId) { - wallet = createWallet(installedWallet.thirdwebId); - } - if (!wallet) { throw new Error("Wallet could not be created"); } diff --git a/ios/Podfile b/ios/Podfile index efd6f949f..254fded31 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -11,13 +11,13 @@ require 'json' podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} plugin 'cocoapods-pod-linkage' -platform :ios, podfile_properties['ios.deploymentTarget'] || '14.0' +platform :ios, podfile_properties['ios.deploymentTarget'] || '15.6' install! 'cocoapods', :deterministic_uuids => false # Version must match version from XMTP Podspec (matching @xmtp/react-native-sdk from package.json) -# https://github.com/xmtp/xmtp-react-native/blob/v2.6.2/ios/XMTPReactNative.podspec#L29 +# https://github.com/xmtp/xmtp-react-native/blob/v3.0.6/ios/XMTPReactNative.podspec#L29 $xmtpVersion = '3.0.6' # Pinning MMKV to 1.3.3 that has included that fix https://github.com/Tencent/MMKV/pull/1222#issuecomment-1905164314 @@ -64,7 +64,7 @@ target 'Converse' do installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.0' - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.6' config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ecf87f213..c740989df 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2868,8 +2868,8 @@ SPEC CHECKSUMS: web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 XMTP: 48d0c71ef732ac4d79c2942902a132bf71661029 XMTPReactNative: 080300cc2cb53ffd117d2808c4d9922357ce1d34 - Yoga: b05994d1933f507b0a28ceaa4fdb968dc18da178 + Yoga: a9ef4f5c2cd79ad812110525ef61048be6a582a4 -PODFILE CHECKSUM: 8de80dcb8fc027dda19e4b9af8900634e49f9b40 +PODFILE CHECKSUM: e3b306b0081266b928275fbd16d3483e5a0ae250 COCOAPODS: 1.16.2 diff --git a/navigation/OnboardingNavigator.tsx b/navigation/OnboardingNavigator.tsx index baa29e63e..8e58fb93c 100644 --- a/navigation/OnboardingNavigator.tsx +++ b/navigation/OnboardingNavigator.tsx @@ -17,6 +17,7 @@ type OnboardingParamList = { OnboardingPrivy: undefined; OnboardingConnectWallet: { address: string; + isSCW: boolean; }; OnboardingNotifications: undefined; OnboardingUserProfile: undefined; diff --git a/polyfills.ts b/polyfills.ts index b21d8c4b2..c9b30e6c4 100644 --- a/polyfills.ts +++ b/polyfills.ts @@ -9,6 +9,7 @@ import "@azure/core-asynciterator-polyfill"; import "react-native-polyfill-globals/auto"; import { Crypto as WebCrypto } from "@peculiar/webcrypto"; import "@stardazed/streams-polyfill"; +import { NativeModules } from "react-native"; // Necessary for @peculiar/webcrypto. if (!global.Buffer) { @@ -19,3 +20,26 @@ if (!global.crypto.subtle) { const webCrypto = new WebCrypto(); (global.crypto as any).subtle = webCrypto.subtle; } + +function randomUUID() { + if (NativeModules.RandomUuid) { + return NativeModules.RandomUuid.getRandomUuid(); + } + + // Expo SDK 48+ + if ( + global.expo && + global.expo.modules && + global.expo.modules.ExpoCrypto && + global.expo.modules.ExpoCrypto.randomUUID + ) { + // ExpoCrypto.randomUUID() sometimes returns uppercase UUIDs, so we convert them to lowercase + return global.expo.modules.ExpoCrypto.randomUUID().toLowerCase(); + } + + throw new Error("No random UUID available"); +} + +if (typeof global.crypto.randomUUID !== "function") { + global.crypto.randomUUID = randomUUID; +} diff --git a/screens/Navigation/Navigation.tsx b/screens/Navigation/Navigation.tsx index 480c4c1f9..daba159e6 100644 --- a/screens/Navigation/Navigation.tsx +++ b/screens/Navigation/Navigation.tsx @@ -53,6 +53,7 @@ export type NavigationParamList = { OnboardingPrivy: undefined; OnboardingConnectWallet: { address: string; + isSCW: boolean; }; OnboardingPrivateKey: undefined; OnboardingNotifications: undefined; @@ -64,6 +65,7 @@ export type NavigationParamList = { NewAccountUserProfile: undefined; NewAccountConnectWallet: { address: string; + isSCW: boolean; }; NewAccountPrivy: undefined; NewAccountPrivateKey: undefined; diff --git a/screens/NewAccount/NewAccountConnectWalletScreen.tsx b/screens/NewAccount/NewAccountConnectWalletScreen.tsx index afa30cf53..d7294b2b6 100644 --- a/screens/NewAccount/NewAccountConnectWalletScreen.tsx +++ b/screens/NewAccount/NewAccountConnectWalletScreen.tsx @@ -11,7 +11,7 @@ export const NewAccountConnectWalletScreen = memo( function NewAccountConnectWalletScreen({ route, }: NativeStackScreenProps) { - const { address } = route.params; + const { address, isSCW } = route.params; const router = useRouter(); @@ -40,6 +40,7 @@ export const NewAccountConnectWalletScreen = memo( {/* For now we don't need to have specific stuff for onboarding vs new account so we use this component to encapsulate the connect view wallet logic */} diff --git a/screens/NewAccount/NewAccountScreen.tsx b/screens/NewAccount/NewAccountScreen.tsx index 04e76355c..f440e23a3 100644 --- a/screens/NewAccount/NewAccountScreen.tsx +++ b/screens/NewAccount/NewAccountScreen.tsx @@ -64,8 +64,8 @@ export const NewAccountScreen = memo(function NewAccountScreen() { // TODO: Add a better message Alert.alert("Account already connected"); }} - onAccountDoesNotExist={({ address }) => { - router.navigate("NewAccountConnectWallet", { address }); + onAccountDoesNotExist={({ address, isSCW }) => { + router.navigate("NewAccountConnectWallet", { address, isSCW }); }} /> )} diff --git a/screens/Onboarding/OnboardingConnectWalletScreen.tsx b/screens/Onboarding/OnboardingConnectWalletScreen.tsx index 1871de6b5..234f8a045 100644 --- a/screens/Onboarding/OnboardingConnectWalletScreen.tsx +++ b/screens/Onboarding/OnboardingConnectWalletScreen.tsx @@ -19,8 +19,7 @@ export const OnboardingConnectWalletScreen = memo( "OnboardingConnectWallet" > ) { - const { address } = props.route.params; - + const { address, isSCW } = props.route.params; const router = useRouter(); const handleDoneConnecting = useCallback(() => { @@ -45,6 +44,7 @@ export const OnboardingConnectWalletScreen = memo( {/* For now we don't need to have specific stuff for onboarding vs new account so we use this component to encapsulate the connect view wallet logic */} diff --git a/screens/Onboarding/OnboardingGetStartedScreen.tsx b/screens/Onboarding/OnboardingGetStartedScreen.tsx index 45ef0c76e..0b024c687 100644 --- a/screens/Onboarding/OnboardingGetStartedScreen.tsx +++ b/screens/Onboarding/OnboardingGetStartedScreen.tsx @@ -85,8 +85,8 @@ export function OnboardingGetStartedScreen() { status: "signedIn", }); }} - onAccountDoesNotExist={({ address }) => { - router.navigate("OnboardingConnectWallet", { address }); + onAccountDoesNotExist={({ address, isSCW }) => { + router.navigate("OnboardingConnectWallet", { address, isSCW }); }} /> diff --git a/utils/date.test.ts b/utils/date.test.ts index 609190325..556065fb8 100644 --- a/utils/date.test.ts +++ b/utils/date.test.ts @@ -1,8 +1,7 @@ -import { format } from "date-fns"; +import { format, enUS, fr } from "date-fns"; import { getLocales } from "react-native-localize"; import { getMinimalDate, getRelativeDate, getRelativeDateTime } from "./date"; -import { enUS, fr } from "date-fns/locale"; jest.mock("react-native-localize", () => ({ getLocales: jest.fn(), diff --git a/utils/evm/external.ts b/utils/evm/external.ts index 0886476ef..bd36e22e2 100644 --- a/utils/evm/external.ts +++ b/utils/evm/external.ts @@ -2,7 +2,7 @@ import { useInstalledWallets } from "@components/Onboarding/ConnectViaWallet/Con import { translate } from "@i18n"; import logger from "@utils/logger"; import { sentryTrackError } from "@utils/sentry"; -import { thirdwebClient } from "@utils/thirdweb"; +import { thirdwebClient, thirdwebWallets } from "@utils/thirdweb"; import { Signer } from "ethers"; import { useCallback, useEffect, useMemo } from "react"; import { Alert } from "react-native"; @@ -183,15 +183,6 @@ export const useAutoConnectExternalWallet = () => { // thirdweb external wallet useAutoConnect({ client: thirdwebClient, - // The Coinbase wallet callbackURL needs to be set - // here also for autoconnect to work - wallets: [ - createWallet("com.coinbase.wallet", { - appMetadata: config.walletConnectConfig.appMetadata, - mobileConfig: { - callbackURL: `https://${config.websiteDomain}/coinbase`, - }, - }), - ], + wallets: Object.values(thirdwebWallets), }); }; diff --git a/utils/evm/wallets.ts b/utils/evm/wallets.ts index b8f9263c6..319d83502 100644 --- a/utils/evm/wallets.ts +++ b/utils/evm/wallets.ts @@ -19,7 +19,9 @@ import { scroll, mantaPacific, xai, + Chain, } from "thirdweb/chains"; +import { WalletId } from "thirdweb/wallets"; export const DEFAULT_SUPPORTED_CHAINS = [ ethereum, @@ -50,3 +52,113 @@ export const CHAIN_BY_ID = DEFAULT_SUPPORTED_CHAINS.reduce( }, {} as Record ); +const _SUPPORTED_WALLETS = { + // Keep this one first to enable auto-connect without conflict with Coinbase Wallet + "Coinbase Smart Wallet": { + iconURL: + "https://explorer-api.walletconnect.com/v3/logo/sm/a5ebc364-8f91-4200-fcc6-be81310a0000?projectId=2f05ae7f1116030fde2d36508f472bfb", + thirdwebId: "com.coinbase.wallet", + isSmartContractWallet: true, + }, + "Coinbase Wallet": { + iconURL: + "https://explorer-api.walletconnect.com/v3/logo/sm/a5ebc364-8f91-4200-fcc6-be81310a0000?projectId=2f05ae7f1116030fde2d36508f472bfb", + customScheme: "cbwallet://", + thirdwebId: "com.coinbase.wallet", + }, + "Ledger Live": { + walletConnectId: + "19177a98252e07ddfc9af2083ba8e07ef627cb6103467ffebb3f8f4205fd7927", + iconURL: + "https://explorer-api.walletconnect.com/v3/logo/sm/a7f416de-aa03-4c5e-3280-ab49269aef00?projectId=2f05ae7f1116030fde2d36508f472bfb", + customScheme: "ledgerlive://", + thirdwebId: "com.ledger", + }, + Rainbow: { + walletConnectId: + "1ae92b26df02f0abca6304df07debccd18262fdf5fe82daa81593582dac9a369", + iconURL: + "https://explorer-api.walletconnect.com/v3/logo/sm/7a33d7f1-3d12-4b5c-f3ee-5cd83cb1b500?projectId=2f05ae7f1116030fde2d36508f472bfb", + customScheme: "rainbow://", + thirdwebId: "me.rainbow", + // Rainbow Mobile does not support tesnets (even Sepolia) + // https://rainbow.me/en/support/app/testnets + supportedChains: [ + ethereum, + base, + optimism, + polygon, + arbitrum, + avalanche, + blast, + zora, + ], + }, + MetaMask: { + walletConnectId: + "c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96", + iconURL: + "https://explorer-api.walletconnect.com/v3/logo/sm/018b2d52-10e9-4158-1fde-a5d5bac5aa00?projectId=2f05ae7f1116030fde2d36508f472bfb", + customScheme: "metamask://", + thirdwebId: "io.metamask", + }, + "Trust Wallet": { + walletConnectId: + "4622a2b2d6af1c9844944291e5e7351a6aa24cd7b23099efac1b2fd875da31a0", + iconURL: + "https://explorer-api.walletconnect.com/v3/logo/sm/0528ee7e-16d1-4089-21e3-bbfb41933100?projectId=2f05ae7f1116030fde2d36508f472bfb", + customScheme: "trust://", + thirdwebId: "com.trustwallet.app", + }, + "Uniswap Wallet": { + walletConnectId: + "c03dfee351b6fcc421b4494ea33b9d4b92a984f87aa76d1663bb28705e95034a", + iconURL: + "https://explorer-api.walletconnect.com/v3/logo/sm/bff9cf1f-df19-42ce-f62a-87f04df13c00?projectId=2f05ae7f1116030fde2d36508f472bfb", + customScheme: "uniswap://", + thirdwebId: "org.uniswap", + }, + Zerion: { + walletConnectId: + "ecc4036f814562b41a5268adc86270fba1365471402006302e70169465b7ac18", + iconURL: + "https://explorer-api.walletconnect.com/v3/logo/sm/73f6f52f-7862-49e7-bb85-ba93ab72cc00?projectId=2f05ae7f1116030fde2d36508f472bfb", + customScheme: "zerion://", + thirdwebId: "io.zerion.wallet", + }, + Exodus: { + walletConnectId: + "e9ff15be73584489ca4a66f64d32c4537711797e30b6660dbcb71ea72a42b1f4", + iconURL: + "https://explorer-api.walletconnect.com/v3/logo/sm/4c16cad4-cac9-4643-6726-c696efaf5200?projectId=2f05ae7f1116030fde2d36508f472bfb", + customScheme: "exodus://", + universalLink: "https://exodus.com/m", + thirdwebId: "com.exodus", + }, +}; + +export type ISupportedWalletName = + | keyof typeof _SUPPORTED_WALLETS + | "EthOS Wallet"; + +export const SUPPORTED_WALLETS = Object.fromEntries( + Object.entries(_SUPPORTED_WALLETS).map(([name, config]) => [ + name, + { + name, + ...config, + }, + ]) +) as Record; + +export type InstalledWallet = { + name: string; + iconURL: string; + customScheme?: string; + universalLink?: string; + walletConnectId?: string; + platforms?: string[]; + thirdwebId?: WalletId; + isSmartContractWallet?: boolean; + supportedChains?: Chain[]; +}; diff --git a/utils/thirdweb.ts b/utils/thirdweb.ts index 9cd0dfc9b..ea12e0b97 100644 --- a/utils/thirdweb.ts +++ b/utils/thirdweb.ts @@ -1,6 +1,8 @@ import { createThirdwebClient } from "thirdweb"; import config from "../config"; +import { createWallet, Wallet, WalletId } from "thirdweb/wallets"; +import { ISupportedWalletName, SUPPORTED_WALLETS } from "./evm/wallets"; const thirdwebGateway = `https://${config.thirdwebClientId}.ipfscdn.io`; @@ -18,3 +20,37 @@ export const getIPFSAssetURI = (ipfsURI?: string) => { export const thirdwebClient = createThirdwebClient({ clientId: config.thirdwebClientId, }); + +export const thirdwebWallets: Record = + Object.fromEntries( + Object.entries(SUPPORTED_WALLETS) + .filter(([_, walletConfig]) => !!walletConfig.thirdwebId) + .map(([walletName, walletConfig]) => { + let wallet: Wallet; + if (walletName === "Coinbase Smart Wallet") { + wallet = createWallet("com.coinbase.wallet", { + appMetadata: config.walletConnectConfig.appMetadata, + mobileConfig: { + callbackURL: `https://${config.websiteDomain}`, + }, + walletConfig: { + options: "smartWalletOnly", + }, + }); + } else if (walletName === "Coinbase Wallet") { + wallet = createWallet("com.coinbase.wallet", { + appMetadata: config.walletConnectConfig.appMetadata, + mobileConfig: { + callbackURL: `https://${config.websiteDomain}/coinbase`, + }, + walletConfig: { + options: "eoaOnly", + }, + }); + } else { + wallet = createWallet(walletConfig.thirdwebId!); + } + + return [walletName, wallet]; + }) + ) as Record; diff --git a/utils/xmtpRN/signIn.ts b/utils/xmtpRN/signIn.ts index 04dec513a..4ddd879e1 100644 --- a/utils/xmtpRN/signIn.ts +++ b/utils/xmtpRN/signIn.ts @@ -20,6 +20,7 @@ export const getInboxId = (address: string) => export const createXmtpClientFromSigner = async ( signer: Signer, + isSCW: boolean, onInstallationRevoked: () => Promise, preAuthenticateToInboxCallback?: () => Promise ) => { @@ -32,13 +33,14 @@ export const createXmtpClientFromSigner = async ( dbDirectory: tempDirectory, dbEncryptionKey, }; + const inboxId = await getInboxId(await signer.getAddress()); await copyDatabasesToTemporaryDirectory(tempDirectory, inboxId); logger.debug("Instantiating client from signer"); - const client = await Client.create(ethersSignerToXmtpSigner(signer), { + const client = await Client.create(ethersSignerToXmtpSigner(signer, isSCW), { ...options, preAuthenticateToInboxCallback, });