diff --git a/.changeset/hot-adults-doubt.md b/.changeset/hot-adults-doubt.md new file mode 100644 index 00000000000..f55fdeb739e --- /dev/null +++ b/.changeset/hot-adults-doubt.md @@ -0,0 +1,5 @@ +--- +"thirdweb": minor +--- + +New `useLinkProfile()` hook to link profiles to inapp and ecosystem accounts diff --git a/apps/playground-web/src/app/connect/in-app-wallet/ecosystem/page.tsx b/apps/playground-web/src/app/connect/in-app-wallet/ecosystem/page.tsx index be07bf4b57d..444cf595b78 100644 --- a/apps/playground-web/src/app/connect/in-app-wallet/ecosystem/page.tsx +++ b/apps/playground-web/src/app/connect/in-app-wallet/ecosystem/page.tsx @@ -1,13 +1,18 @@ +export const dynamic = "force-dynamic"; import { CodeExample } from "@/components/code/code-example"; import { EcosystemConnectEmbed } from "../../../../components/in-app-wallet/ecosystem"; +import { Profiles } from "../../../../components/in-app-wallet/profile-sections"; import ThirdwebProvider from "../../../../components/thirdweb-provider"; export default function Page() { return ( -
+
+
+ +
); } @@ -17,7 +22,7 @@ function AnyAuth() { <>

- Your own Ecosystem + Build your own Ecosystem

Build a public or permissioned ecosystem by allowing third party apps diff --git a/apps/playground-web/src/app/connect/in-app-wallet/page.tsx b/apps/playground-web/src/app/connect/in-app-wallet/page.tsx index 6d04cf03308..ba6e4f75f6d 100644 --- a/apps/playground-web/src/app/connect/in-app-wallet/page.tsx +++ b/apps/playground-web/src/app/connect/in-app-wallet/page.tsx @@ -1,5 +1,7 @@ +export const dynamic = "force-dynamic"; import { CodeExample } from "@/components/code/code-example"; import { InAppConnectEmbed } from "../../../components/in-app-wallet/connect-button"; +import { Profiles } from "../../../components/in-app-wallet/profile-sections"; import ThirdwebProvider from "../../../components/thirdweb-provider"; export default function Page() { @@ -8,6 +10,9 @@ export default function Page() {

+
+ +
); } diff --git a/apps/playground-web/src/app/connect/sign-in/components/CodeGen.tsx b/apps/playground-web/src/app/connect/sign-in/components/CodeGen.tsx index e96347e35dc..6c417ec12c4 100644 --- a/apps/playground-web/src/app/connect/sign-in/components/CodeGen.tsx +++ b/apps/playground-web/src/app/connect/sign-in/components/CodeGen.tsx @@ -1,19 +1,11 @@ import { Suspense, lazy } from "react"; -import { LoadingDots } from "../../../../components/ui/LoadingDots"; +import { CodeLoading } from "../../../../components/code/code.client"; import type { ConnectPlaygroundOptions } from "./types"; const CodeClient = lazy( () => import("../../../../components/code/code.client"), ); -function CodeLoading() { - return ( -
- -
- ); -} - export function CodeGen(props: { connectOptions: ConnectPlaygroundOptions; }) { diff --git a/apps/playground-web/src/app/navLinks.ts b/apps/playground-web/src/app/navLinks.ts index 4fe0f471f77..40d801973a6 100644 --- a/apps/playground-web/src/app/navLinks.ts +++ b/apps/playground-web/src/app/navLinks.ts @@ -42,11 +42,11 @@ export const navLinks: SidebarLink[] = [ expanded: true, links: [ { - name: "Any auth method", + name: "Any Auth", href: "/connect/in-app-wallet", }, { - name: "Your own Ecosystem", + name: "Ecosystems", href: "/connect/in-app-wallet/ecosystem", }, { diff --git a/apps/playground-web/src/components/code/code.client.tsx b/apps/playground-web/src/components/code/code.client.tsx index 2a482993487..833849f96f9 100644 --- a/apps/playground-web/src/components/code/code.client.tsx +++ b/apps/playground-web/src/components/code/code.client.tsx @@ -1,5 +1,6 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query"; import type { BundledLanguage } from "shiki"; +import { LoadingDots } from "../ui/LoadingDots"; import { RenderCode } from "./RenderCode"; import { getCodeHtml } from "./getCodeHtml"; @@ -14,6 +15,14 @@ export type CodeProps = { scrollableClassName?: string; }; +export function CodeLoading() { + return ( +
+ +
+ ); +} + export const CodeClient: React.FC = ({ code, lang, diff --git a/apps/playground-web/src/components/in-app-wallet/profile-sections.tsx b/apps/playground-web/src/components/in-app-wallet/profile-sections.tsx new file mode 100644 index 00000000000..640c56b1c2e --- /dev/null +++ b/apps/playground-web/src/components/in-app-wallet/profile-sections.tsx @@ -0,0 +1,78 @@ +import { CodeExample } from "@/components/code/code-example"; +import { LinkAccount, LinkedAccounts } from "./profiles"; + +export function Profiles() { + return ( + <> +
+

+ View Linked Profiles +

+

+ View all web2 and web3 linked profiles for a user along with specific + details for each profile type, including name, email, profile picture + and more. +

+
+ + } + code={`import { useProfiles } from "thirdweb/react"; + + function App() { + const { data: profiles } = useProfiles({ + client, + }); + + return ( +
+ {profiles?.map((profile) => ( +
+ +
+ ))} +
+ ); + };`} + lang="tsx" + /> + +
+

+ Link another profile +

+

+ Link a web2 or web3 profile to the connected account. +
+ You can do this with hooks like shown here or from the prebuilt + connect UI. +

+
+ + } + code={`import { useLinkProfile } from "thirdweb/react"; + + function App() { + const { mutate: linkProfile, isPending, error } = useLinkProfile(); + + return ( +
+ +
+ ); + };`} + lang="tsx" + /> + + ); +} diff --git a/apps/playground-web/src/components/in-app-wallet/profiles.tsx b/apps/playground-web/src/components/in-app-wallet/profiles.tsx new file mode 100644 index 00000000000..235536e8558 --- /dev/null +++ b/apps/playground-web/src/components/in-app-wallet/profiles.tsx @@ -0,0 +1,92 @@ +"use client"; +import { baseSepolia } from "thirdweb/chains"; +import { useActiveAccount, useLinkProfile, useProfiles } from "thirdweb/react"; +import { type WalletId, createWallet } from "thirdweb/wallets"; +import { THIRDWEB_CLIENT } from "../../lib/client"; +import CodeClient, { CodeLoading } from "../code/code.client"; +import { Button } from "../ui/button"; + +export function LinkedAccounts() { + const { data: profiles } = useProfiles({ + client: THIRDWEB_CLIENT, + }); + + return ( +
+ {profiles ? ( + } + /> + ) : ( +

Login to see linked profiles

+ )} +
+ ); +} + +export function LinkAccount() { + const { mutate: linkProfile, isPending, error } = useLinkProfile(); + const account = useActiveAccount(); + const linkWallet = async (walletId: WalletId) => { + linkProfile({ + client: THIRDWEB_CLIENT, + strategy: "wallet", + wallet: createWallet(walletId), + chain: baseSepolia, + }); + }; + + const linkPasskey = async () => { + linkProfile({ + client: THIRDWEB_CLIENT, + strategy: "passkey", + type: "sign-up", + }); + }; + + return ( +
+ {account ? ( + <> + {isPending ? ( +

Linking...

+ ) : ( + <> + {/* + TODO make cb smart wallet linking work + */} + + + + )} + {error &&

Error: {error.message}

} + + ) : ( +

Login to link another account.

+ )} +
+ ); +} diff --git a/apps/portal/src/app/react-native/v5/sidebar.tsx b/apps/portal/src/app/react-native/v5/sidebar.tsx index c6700df17b2..da7498f7634 100644 --- a/apps/portal/src/app/react-native/v5/sidebar.tsx +++ b/apps/portal/src/app/react-native/v5/sidebar.tsx @@ -121,6 +121,16 @@ export const sidebar: SideBar = { }, ], }, + { + name: "Ecosystem Wallets", + links: [ + { + name: "React API", + href: "/react/v5/ecosystem-wallet/get-started", + icon: , + }, + ], + }, { name: "Account Abstraction", links: [ diff --git a/apps/portal/src/app/react/v5/ecosystem-wallet/get-started/page.mdx b/apps/portal/src/app/react/v5/ecosystem-wallet/get-started/page.mdx new file mode 100644 index 00000000000..be5de5180f8 --- /dev/null +++ b/apps/portal/src/app/react/v5/ecosystem-wallet/get-started/page.mdx @@ -0,0 +1,72 @@ +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent, + DocImage, + createMetadata, +} from "@doc"; + +export const metadata = createMetadata({ + title: "Connect users with Ecosystem Wallets", + description: + "use the prebuilt connect UI components to authenticate users and connect ecosystem wallets", + image: { + title: "Connect users with Ecosystem Wallets", + icon: "wallets", + }, +}); + +# Connect Users to your Ecosystem + +## Using the Connect UI components + +If you're building a [React website](/typescript/react/v5/ConnectButton), [React Native app](/react-native/v5/), or [Unity game](/unity/ConnectWallet) you can use the prebuilt connect UI components to authenticate users and connect their wallets accross your ecosystem. + +```jsx +import { ThirdwebProvider, ConnectButton } from "thirdweb/react"; +import { inAppWallet } from "thirdweb/wallets"; + +const client = createThirdwebClient({ clientId }); +const wallets = [ecosystemWallet("ecosystem.your-ecosystem-id")]; + +export default function App() { + return ( + + + + ); +} +``` + +## Using your own UI + +You can also build your own UI using the low-level hooks and functions. Remember to wrap your app in a `ThirdwebProvider` to ensure that the wallet is available to all components in your app. + +```tsx +import { ecosystemWallet } from "thirdweb/wallets"; + +const wallet = ecosystemWallet("ecosystem.your-ecosystem-id"); + +const LoginComponent = () => { + const { connect, isLoading } = useConnect(); + + return ; +}; +``` + +## Passing a partner ID + +For closed ecosystems, you can pass a valid `partnerId` to the `ecosystemWallet` provided by the ecosystem owner. + +```tsx +const wallet = ecosystemWallet("ecosystem.your-ecosystem-id", { + partnerId: "your-partner-id", +}); +``` diff --git a/apps/portal/src/app/react/v5/sidebar.tsx b/apps/portal/src/app/react/v5/sidebar.tsx index 5891d1ca79b..301efd26c78 100644 --- a/apps/portal/src/app/react/v5/sidebar.tsx +++ b/apps/portal/src/app/react/v5/sidebar.tsx @@ -148,6 +148,11 @@ export const sidebar: SideBar = { { name: "In-App Wallets", links: [ + { + name: "Playground", + href: "https://playground.thirdweb.com/connect/in-app-wallet", + icon: , + }, { name: "Get Started", href: `${slug}/in-app-wallet/get-started`, @@ -171,8 +176,34 @@ export const sidebar: SideBar = { ...[ "inAppWallet", "preAuthenticate", - "getUserEmail", - "getUserPhoneNumber", + "useLinkProfile", + "useProfiles", + "hasStoredPasskey", + ].map((name) => ({ + name, + href: `${slug}/${name}`, + icon: , + })), + ], + }, + { + name: "Ecosystem Wallets", + links: [ + { + name: "Playground", + href: "https://playground.thirdweb.com/connect/ecosystem", + icon: , + }, + { + name: "Get Started", + href: `${slug}/ecosystem-wallet/get-started`, + icon: , + }, + ...[ + "ecosystemWallet", + "preAuthenticate", + "useLinkProfile", + "useProfiles", "hasStoredPasskey", ].map((name) => ({ name, @@ -184,6 +215,11 @@ export const sidebar: SideBar = { { name: "Account Abstraction", links: [ + { + name: "Playground", + href: "https://playground.thirdweb.com/connect/account-abstraction", + icon: , + }, { name: "Get Started", href: `${slug}/account-abstraction/get-started`, diff --git a/apps/portal/src/app/typescript/v5/sidebar.tsx b/apps/portal/src/app/typescript/v5/sidebar.tsx index 4a06d5ac565..4cceeca5e3c 100644 --- a/apps/portal/src/app/typescript/v5/sidebar.tsx +++ b/apps/portal/src/app/typescript/v5/sidebar.tsx @@ -96,8 +96,8 @@ export const sidebar: SideBar = { links: [ "inAppWallet", "preAuthenticate", - "getUserEmail", - "getUserPhoneNumber", + "linkProfile", + "getProfiles", "hasStoredPasskey", ].map((name) => ({ name, @@ -110,8 +110,8 @@ export const sidebar: SideBar = { links: [ "ecosystemWallet", "preAuthenticate", - "getUserEmail", - "getUserPhoneNumber", + "linkProfile", + "getProfiles", "hasStoredPasskey", ].map((name) => ({ name, diff --git a/packages/thirdweb/src/exports/react.native.ts b/packages/thirdweb/src/exports/react.native.ts index 91780b9b6b7..c8a4275361a 100644 --- a/packages/thirdweb/src/exports/react.native.ts +++ b/packages/thirdweb/src/exports/react.native.ts @@ -26,6 +26,7 @@ export { useSwitchActiveWalletChain } from "../react/core/hooks/wallets/useSwitc export { useCallsStatus } from "../react/core/hooks/wallets/useCallsStatus.js"; export { useWalletBalance } from "../react/core/hooks/others/useWalletBalance.js"; export { useProfiles } from "../react/native/hooks/wallets/useProfiles.js"; +export { useLinkProfile } from "../react/native/hooks/wallets/useLinkProfile.js"; // contract export { useReadContract } from "../react/core/hooks/contract/useReadContract.js"; diff --git a/packages/thirdweb/src/exports/react.ts b/packages/thirdweb/src/exports/react.ts index b013903a9a3..8714932cb09 100644 --- a/packages/thirdweb/src/exports/react.ts +++ b/packages/thirdweb/src/exports/react.ts @@ -57,6 +57,7 @@ export { useSwitchActiveWalletChain } from "../react/core/hooks/wallets/useSwitc export { useCallsStatus } from "../react/core/hooks/wallets/useCallsStatus.js"; export { useWalletBalance } from "../react/core/hooks/others/useWalletBalance.js"; export { useProfiles } from "../react/web/hooks/wallets/useProfiles.js"; +export { useLinkProfile } from "../react/web/hooks/wallets/useLinkProfile.js"; // chain hooks export { useChainMetadata } from "../react/core/hooks/others/useChainQuery.js"; diff --git a/packages/thirdweb/src/exports/wallets.native.ts b/packages/thirdweb/src/exports/wallets.native.ts index 8a27b4035b5..50783fc7489 100644 --- a/packages/thirdweb/src/exports/wallets.native.ts +++ b/packages/thirdweb/src/exports/wallets.native.ts @@ -99,6 +99,7 @@ export { getProfiles, linkProfile, } from "../wallets/in-app/native/auth/index.js"; +export type { Profile } from "../wallets/in-app/core/authentication/types.js"; export const authenticateWithRedirect = () => { throw new Error("Not supported in native"); }; diff --git a/packages/thirdweb/src/exports/wallets.ts b/packages/thirdweb/src/exports/wallets.ts index 16aa0a50d18..e02c64f2a40 100644 --- a/packages/thirdweb/src/exports/wallets.ts +++ b/packages/thirdweb/src/exports/wallets.ts @@ -106,6 +106,7 @@ export { getProfiles, linkProfile, } from "../wallets/in-app/web/lib/auth/index.js"; +export type { Profile } from "../wallets/in-app/core/authentication/types.js"; export { getUser, diff --git a/packages/thirdweb/src/extensions/thirdweb/write/publish.test.ts b/packages/thirdweb/src/extensions/thirdweb/write/publish.test.ts index 9ec5d612128..540b306b873 100644 --- a/packages/thirdweb/src/extensions/thirdweb/write/publish.test.ts +++ b/packages/thirdweb/src/extensions/thirdweb/write/publish.test.ts @@ -7,21 +7,25 @@ import { CONTRACT_PUBLISHER_ADDRESS } from "../../../contract/deployment/publish import { parseEventLogs } from "../../../event/actions/parse-logs.js"; import { download } from "../../../storage/download.js"; import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js"; -import { fetchDeployMetadata } from "../../../utils/any-evm/deploy-metadata.js"; +import { + type FetchDeployMetadataResult, + fetchDeployMetadata, +} from "../../../utils/any-evm/deploy-metadata.js"; import { contractPublishedEvent } from "../__generated__/IContractPublisher/events/ContractPublished.js"; import { getAllPublishedContracts } from "../__generated__/IContractPublisher/read/getAllPublishedContracts.js"; import { getPublishedContractVersions } from "../__generated__/IContractPublisher/read/getPublishedContractVersions.js"; import { publishContract } from "./publish.js"; -describe.runIf(process.env.TW_SECRET_KEY)("publishContract", () => { - it("should publish a contract successfully", async () => { - const publisherContract = getContract({ - client: TEST_CLIENT, - chain: FORKED_POLYGON_CHAIN, - address: CONTRACT_PUBLISHER_ADDRESS, - }); +describe.runIf(process.env.TW_SECRET_KEY).sequential("publishContract", () => { + const publisherContract = getContract({ + client: TEST_CLIENT, + chain: FORKED_POLYGON_CHAIN, + address: CONTRACT_PUBLISHER_ADDRESS, + }); + let publishedData: FetchDeployMetadataResult; - let publishedContracts = await getAllPublishedContracts({ + it("should publish a contract successfully", async () => { + const publishedContracts = await getAllPublishedContracts({ contract: publisherContract, publisher: TEST_ACCOUNT_D.address, }); @@ -80,7 +84,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("publishContract", () => { "version": "0.0.1", } `); - const publishedData = await fetchDeployMetadata({ + publishedData = await fetchDeployMetadata({ client: TEST_CLIENT, uri: logs?.[0]?.args.publishedContract.publishMetadataUri ?? "", }); @@ -91,8 +95,10 @@ describe.runIf(process.env.TW_SECRET_KEY)("publishContract", () => { expect(publishedData.description).toBe("Cat Attack NFT"); expect(publishedData.publisher).toBe(TEST_ACCOUNT_D.address); expect(publishedData.routerType).toBe("none"); + }, 120000); - publishedContracts = await getAllPublishedContracts({ + it("should throw if publishing the same version", async () => { + const publishedContracts = await getAllPublishedContracts({ contract: publisherContract, publisher: TEST_ACCOUNT_D.address, }); @@ -114,7 +120,9 @@ describe.runIf(process.env.TW_SECRET_KEY)("publishContract", () => { }), }), ).rejects.toThrow("Version 0.0.1 is not greater than 0.0.1"); + }); + it("should publish a new version", async () => { const tx2 = publishContract({ contract: publisherContract, account: TEST_ACCOUNT_D, @@ -143,7 +151,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("publishContract", () => { }); expect(publishedData2.version).toBe("0.0.2"); - publishedContracts = await getAllPublishedContracts({ + const publishedContracts = await getAllPublishedContracts({ contract: publisherContract, publisher: TEST_ACCOUNT_D.address, }); diff --git a/packages/thirdweb/src/react/native/hooks/wallets/useLinkProfile.ts b/packages/thirdweb/src/react/native/hooks/wallets/useLinkProfile.ts new file mode 100644 index 00000000000..99392af4353 --- /dev/null +++ b/packages/thirdweb/src/react/native/hooks/wallets/useLinkProfile.ts @@ -0,0 +1,92 @@ +import { useMutation } from "@tanstack/react-query"; +import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js"; +import type { AuthArgsType } from "../../../../wallets/in-app/core/authentication/types.js"; +import type { Ecosystem } from "../../../../wallets/in-app/core/wallet/types.js"; +import { linkProfile } from "../../../../wallets/in-app/web/lib/auth/index.js"; +import { useAdminWallet } from "../../../core/hooks/wallets/useAdminWallet.js"; + +/** + * Links a web2 or web3 profile to the connected in-app or ecosystem account. + * + * **When a profile is linked to the account, that profile can then be used to sign into the same account.** + * + * @example + * + * ### Linking a social profile + * + * ```jsx + * import { useLinkProfile } from "thirdweb/react"; + * + * const { mutate: linkProfile } = useLinkProfile(); + * + * const onClick = () => { + * linkProfile({ + * client, + * strategy: "discord", // or "google", "x", "telegram", etc + * }); + * }; + * ``` + * + * ### Linking an email + * + * ```jsx + * import { useLinkProfile } from "thirdweb/react"; + * import { preAuthenticate } from "thirdweb/wallets"; + * + * const { mutate: linkProfile } = useLinkProfile(); + * + * // send a verification email first + * const sendEmail = async () => { + * const email = await preAuthenticate({ + * client, + * strategy: "email", + * email: "john.doe@example.com", + * }); + * }; + * + * // then link the profile with the verification code + * const onClick = (code: string) => { + * linkProfile({ + * client, + * strategy: "email", + * email: "john.doe@example.com", + * verificationCode: code, + * }); + * }; + * ``` + * + * The same process can be used for phone and email, simply swap out the `strategy` parameter. + * + * ### Linking a wallet + * + * ```jsx + * import { useLinkProfile } from "thirdweb/react"; + * + * const { mutate: linkProfile } = useLinkProfile(); + * + * const onClick = () => { + * linkProfile({ + * client, + * strategy: "wallet", + * wallet: createWallet("io.metamask"), // autocompletion for 400+ wallet ids + * chain: sepolia, // any chain works, needed for SIWE signature + * }); + * }; + * ``` + * + * @wallet + */ +export function useLinkProfile() { + const wallet = useAdminWallet(); + return useMutation({ + mutationKey: ["profiles"], + mutationFn: async (options: Omit) => { + const ecosystem: Ecosystem | undefined = + wallet && isEcosystemWallet(wallet) + ? { id: wallet.id, partnerId: wallet.getConfig()?.partnerId } + : undefined; + const optionsWithEcosystem = { ...options, ecosystem } as AuthArgsType; + return linkProfile(optionsWithEcosystem); + }, + }); +} diff --git a/packages/thirdweb/src/react/native/hooks/wallets/useProfiles.ts b/packages/thirdweb/src/react/native/hooks/wallets/useProfiles.ts index 203290caaab..1334dd04c74 100644 --- a/packages/thirdweb/src/react/native/hooks/wallets/useProfiles.ts +++ b/packages/thirdweb/src/react/native/hooks/wallets/useProfiles.ts @@ -7,11 +7,10 @@ import { getProfiles } from "../../../../wallets/in-app/web/lib/auth/index.js"; import { useAdminWallet } from "../../../core/hooks/wallets/useAdminWallet.js"; /** - * Retrieves all linked profiles of the connected in-app or ecosystem wallet. + * Retrieves all linked profiles of the connected in-app or ecosystem account. * - * @returns A React Query result containing the linked profiles for the connected in-app wallet. - * - * @note This hook will only run if the connected wallet supports multi-auth (in-app wallets). + * @returns A React Query result containing the linked profiles for the connected in-app account. + * @note This hook will only run if the connected wallet supports account linking. * * @example * ```jsx diff --git a/packages/thirdweb/src/react/web/hooks/wallets/useLinkProfile.ts b/packages/thirdweb/src/react/web/hooks/wallets/useLinkProfile.ts new file mode 100644 index 00000000000..c0b27cd30ba --- /dev/null +++ b/packages/thirdweb/src/react/web/hooks/wallets/useLinkProfile.ts @@ -0,0 +1,91 @@ +import { useMutation } from "@tanstack/react-query"; +import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js"; +import type { AuthArgsType } from "../../../../wallets/in-app/core/authentication/types.js"; +import type { Ecosystem } from "../../../../wallets/in-app/core/wallet/types.js"; +import { linkProfile } from "../../../../wallets/in-app/web/lib/auth/index.js"; +import { useAdminWallet } from "../../../core/hooks/wallets/useAdminWallet.js"; + +/** + * Links a web2 or web3 profile to the connected in-app or ecosystem account. + * **When a profile is linked to the account, that profile can then be used to sign into the same account.** + * + * @example + * + * ### Linking a social profile + * + * ```jsx + * import { useLinkProfile } from "thirdweb/react"; + * + * const { mutate: linkProfile } = useLinkProfile(); + * + * const onClick = () => { + * linkProfile({ + * client, + * strategy: "discord", // or "google", "x", "telegram", etc + * }); + * }; + * ``` + * + * ### Linking an email + * + * ```jsx + * import { useLinkProfile } from "thirdweb/react"; + * import { preAuthenticate } from "thirdweb/wallets"; + * + * const { mutate: linkProfile } = useLinkProfile(); + * + * // send a verification email first + * const sendEmail = async () => { + * const email = await preAuthenticate({ + * client, + * strategy: "email", + * email: "john.doe@example.com", + * }); + * }; + * + * // then link the profile with the verification code + * const onClick = (code: string) => { + * linkProfile({ + * client, + * strategy: "email", + * email: "john.doe@example.com", + * verificationCode: code, + * }); + * }; + * ``` + * + * The same process can be used for phone and email, simply swap out the `strategy` parameter. + * + * ### Linking a wallet + * + * ```jsx + * import { useLinkProfile } from "thirdweb/react"; + * + * const { mutate: linkProfile } = useLinkProfile(); + * + * const onClick = () => { + * linkProfile({ + * client, + * strategy: "wallet", + * wallet: createWallet("io.metamask"), // autocompletion for 400+ wallet ids + * chain: sepolia, // any chain works, needed for SIWE signature + * }); + * }; + * ``` + * + * @wallet + */ +export function useLinkProfile() { + const wallet = useAdminWallet(); + return useMutation({ + mutationKey: ["profiles"], + mutationFn: async (options: AuthArgsType) => { + const ecosystem: Ecosystem | undefined = + wallet && isEcosystemWallet(wallet) + ? { id: wallet.id, partnerId: wallet.getConfig()?.partnerId } + : undefined; + const optionsWithEcosystem = { ...options, ecosystem } as AuthArgsType; + return linkProfile(optionsWithEcosystem); + }, + }); +} diff --git a/packages/thirdweb/src/react/web/hooks/wallets/useProfiles.ts b/packages/thirdweb/src/react/web/hooks/wallets/useProfiles.ts index 203290caaab..1334dd04c74 100644 --- a/packages/thirdweb/src/react/web/hooks/wallets/useProfiles.ts +++ b/packages/thirdweb/src/react/web/hooks/wallets/useProfiles.ts @@ -7,11 +7,10 @@ import { getProfiles } from "../../../../wallets/in-app/web/lib/auth/index.js"; import { useAdminWallet } from "../../../core/hooks/wallets/useAdminWallet.js"; /** - * Retrieves all linked profiles of the connected in-app or ecosystem wallet. + * Retrieves all linked profiles of the connected in-app or ecosystem account. * - * @returns A React Query result containing the linked profiles for the connected in-app wallet. - * - * @note This hook will only run if the connected wallet supports multi-auth (in-app wallets). + * @returns A React Query result containing the linked profiles for the connected in-app account. + * @note This hook will only run if the connected wallet supports account linking. * * @example * ```jsx diff --git a/packages/thirdweb/src/utils/any-evm/zksync/isZkSyncChain.ts b/packages/thirdweb/src/utils/any-evm/zksync/isZkSyncChain.ts index 4b6901f7f39..7755a73cfe3 100644 --- a/packages/thirdweb/src/utils/any-evm/zksync/isZkSyncChain.ts +++ b/packages/thirdweb/src/utils/any-evm/zksync/isZkSyncChain.ts @@ -15,7 +15,8 @@ export async function isZkSyncChain(chain: Chain) { chain.id === 282 || chain.id === 388 || chain.id === 4654 || - chain.id === 333271 + chain.id === 333271 || + chain.id === 37111 ) { return true; } diff --git a/packages/thirdweb/src/wallets/in-app/core/authentication/types.ts b/packages/thirdweb/src/wallets/in-app/core/authentication/types.ts index 73f5d31f33f..01118e9f4fd 100644 --- a/packages/thirdweb/src/wallets/in-app/core/authentication/types.ts +++ b/packages/thirdweb/src/wallets/in-app/core/authentication/types.ts @@ -91,6 +91,7 @@ export type OAuthRedirectObject = { redirectUrl: string; }; +// TODO: type this better for each auth provider export type Profile = { type: AuthOption | "wallet"; details: { diff --git a/packages/thirdweb/src/wallets/in-app/core/users/getUser.ts b/packages/thirdweb/src/wallets/in-app/core/users/getUser.ts index 5058a8b48d1..e18d211df63 100644 --- a/packages/thirdweb/src/wallets/in-app/core/users/getUser.ts +++ b/packages/thirdweb/src/wallets/in-app/core/users/getUser.ts @@ -3,6 +3,7 @@ import { getThirdwebBaseUrl } from "../../../../utils/domains.js"; import { getClientFetch } from "../../../../utils/fetch.js"; import type { OneOf, Prettify } from "../../../../utils/type-utils.js"; import type { Profile } from "../authentication/types.js"; +import type { Ecosystem } from "../wallet/types.js"; export type GetUserResult = { userId: string; @@ -44,6 +45,7 @@ export async function getUser({ email, phone, id, + ecosystem, }: Prettify< { client: ThirdwebClient; @@ -52,6 +54,7 @@ export async function getUser({ email?: string; phone?: string; id?: string; + ecosystem?: Ecosystem; }> >): Promise { if (!client.secretKey) { @@ -82,7 +85,7 @@ export async function getUser({ ); } - const clientFetch = getClientFetch(client); + const clientFetch = getClientFetch(client, ecosystem); const res = await clientFetch(url.toString()); diff --git a/packages/thirdweb/src/wallets/in-app/native/native-connector.ts b/packages/thirdweb/src/wallets/in-app/native/native-connector.ts index 402985cefbb..37f5c620fd4 100644 --- a/packages/thirdweb/src/wallets/in-app/native/native-connector.ts +++ b/packages/thirdweb/src/wallets/in-app/native/native-connector.ts @@ -288,6 +288,7 @@ export class InAppNativeConnector implements InAppConnector { client: args.client, tokenToLink: storedToken.cookieString, storage: this.localStorage, + ecosystem: args.ecosystem || this.ecosystem, }); } diff --git a/packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts b/packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts index d129520a2fc..d451a80fed2 100644 --- a/packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts +++ b/packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts @@ -152,9 +152,9 @@ export async function authenticate(args: AuthArgsType) { * @returns A promise that resolves to the authentication result. * @example * ```ts - * import { authenticate } from "thirdweb/wallets/in-app"; + * import { authenticateWithRedirect } from "thirdweb/wallets/in-app"; * - * const result = await authenticate({ + * const result = await authenticateWithRedirect({ * client, * strategy: "google", * mode: "redirect", @@ -180,25 +180,21 @@ export async function authenticateWithRedirect( } /** - * Connects a new profile (authentication method) to the current user. - * The connected profile can be any valid in-app wallet including email, phone, passkey, etc. - * The inputs mirror those used when authenticating normally. + * Connects a new profile (and new authentication method) to the current user. * - * **When a profile is linked to the account, that profile can then be used to sign into the account.** + * Requires a connected in-app or ecosystem account. * - * This method is only available for in-app wallets. + * **When a profile is linked to the account, that profile can then be used to sign into the same account.** * - * @param wallet - The wallet to link an additional profile to. * @param auth - The authentications options to add the new profile. * @returns A promise that resolves to the currently linked profiles when the connection is successful. * @throws If the connection fails, if the profile is already linked to the account, or if the profile is already associated with another account. * * @example * ```ts - * const wallet = inAppWallet(); + * import { linkProfile } from "thirdweb/wallets"; * - * await wallet.connect({ client, strategy: "google" }); - * const profiles = await linkProfile({ client, strategy: "discord" }); + * await linkProfile({ client, strategy: "discord" }); * ``` * @wallet */ @@ -214,17 +210,28 @@ export async function linkProfile(args: AuthArgsType) { * * @example * ```ts - * import { inAppWallet } from "thirdweb/wallets"; - * - * const wallet = inAppWallet(); - * wallet.connect({ strategy: "google" }); + * import { getProfiles } from "thirdweb/wallets"; * * const profiles = await getProfiles({ * client, * }); * - * console.log(profiles[0].type); + * console.log(profiles[0].type); // will be "email", "phone", "google", "discord", etc * console.log(profiles[0].details.email); + * console.log(profiles[0].details.phone); + * ``` + * + * ### Getting profiles for a ecosystem user + * + * ```ts + * import { getProfiles } from "thirdweb/wallets/in-app"; + * + * const profiles = await getProfiles({ + * client, + * ecosystem: { + * id: "ecosystem.your-ecosystem-id", + * }, + * }); * ``` * @wallet */ diff --git a/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts b/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts index 3c6ee8f6bf3..70a2c230f4c 100644 --- a/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts +++ b/packages/thirdweb/src/wallets/in-app/web/lib/web-connector.ts @@ -444,6 +444,7 @@ export class InAppWebConnector implements InAppConnector { client: args.client, tokenToLink: storedToken.cookieString, storage: this.localStorage, + ecosystem: args.ecosystem || this.ecosystem, }); } diff --git a/packages/thirdweb/test/globalSetup.ts b/packages/thirdweb/test/globalSetup.ts index 182705c8c68..9c2f5d01269 100644 --- a/packages/thirdweb/test/globalSetup.ts +++ b/packages/thirdweb/test/globalSetup.ts @@ -64,13 +64,17 @@ export default async function globalSetup() { port: 8648, }); + // TODO re-enable thirdweb RPC for this fork + // forkUrl: SECRET_KEY + // ? `https://137.rpc.thirdweb.com/${clientId}` + // : "https://polygon-rpc.com", + // forkHeader: SECRET_KEY ? { "x-secret-key": SECRET_KEY } : {}, const shutdownPolygon = await startProxy({ port: 8649, options: { chainId: 137, - forkUrl: SECRET_KEY - ? `https://137.rpc.thirdweb.com/${clientId}` - : "https://polygon-rpc.com", + // using public rpc for now + forkUrl: "https://polygon-rpc.com", forkHeader: SECRET_KEY ? { "x-secret-key": SECRET_KEY } : {}, forkChainId: 137, forkBlockNumber: POLYGON_FORK_BLOCK_NUMBER,