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 (
+
+ linkProfile({
+ client: THIRDWEB_CLIENT,
+ strategy: "wallet",
+ wallet: createWallet("com.coinbase.wallet"),
+ chain: baseSepolia,
+ })}
+ >
+ Link Coinbase Wallet
+
+
+ );
+ };`}
+ 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
+
linkWallet("com.coinbase.wallet")}
+ className="rounded-full p-6"
+ disabled={isPending}
+ >
+ Link Coinbase Wallet
+ */}
+
linkWallet("io.metamask")}
+ className="rounded-full p-6"
+ disabled={isPending}
+ >
+ Link MetaMask
+
+
+ Link Passkey
+
+ >
+ )}
+ {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 connect(async () => {
+ await wallet.connect({
+ client,
+ strategy: "discord", // or any supported auth strategy
+ })
+ return wallet;
+ })}>Connect ;
+};
+```
+
+## 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,