[SDK] Feature: Ecosystem account linking (#4733)

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR enables account linking for ecosystem wallets.

### Detailed summary
- Added support for ecosystem wallets in account linking functionality
- Updated conditions for multi-auth support in various components
- Imported and utilized `isEcosystemWallet` function in multiple files
- Modified functions to handle both in-app and ecosystem wallets in account linking operations

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
gregfromstl
2024-09-21 01:01:07 +00:00
parent 6e3669d384
commit 824b1bdb69
10 changed files with 129 additions and 74 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---
Enable account linking for ecosystem wallets
@@ -1,4 +1,5 @@
import { type UseQueryResult, useQuery } from "@tanstack/react-query";
import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js";
import type { Profile } from "../../../../wallets/in-app/core/authentication/types.js";
import { getProfiles } from "../../../../wallets/in-app/core/wallet/profiles.js";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
@@ -28,7 +29,7 @@ export function useProfiles(): UseQueryResult<Profile[]> {
return useQuery({
queryKey: ["profiles", wallet?.id],
enabled: !!wallet && wallet.id === "inApp",
enabled: !!wallet && (wallet.id === "inApp" || isEcosystemWallet(wallet)),
queryFn: async () => {
return getProfiles(wallet as Wallet<"inApp">);
},
@@ -3,10 +3,13 @@ import { CrossCircledIcon } from "@radix-ui/react-icons";
import { useQueryClient } from "@tanstack/react-query";
import { Suspense, lazy } from "react";
import type { ThirdwebClient } from "../../../../../client/client.js";
import { isEcosystemWallet } from "../../../../../wallets/ecosystem/is-ecosystem-wallet.js";
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
import type { EcosystemWalletId } from "../../../../../wallets/wallet-types.js";
import { iconSize } from "../../../../core/design-system/index.js";
import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js";
import { useActiveWalletChain } from "../../../../core/hooks/wallets/useActiveWalletChain.js";
import EcosystemWalletConnectUI from "../../../wallets/ecosystem/EcosystemWalletConnectUI.js";
import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js";
import { Container, Line, ModalHeader } from "../../components/basic.js";
import { Text } from "../../components/text.js";
@@ -58,6 +61,30 @@ export function LinkProfileScreen(props: {
);
}
if (isEcosystemWallet(activeWallet)) {
return (
<Suspense fallback={<LoadingScreen />}>
<EcosystemWalletConnectUI
wallet={activeWallet as Wallet<EcosystemWalletId>}
done={() => {
queryClient.invalidateQueries({ queryKey: ["profiles"] });
props.onBack();
}}
connectLocale={props.locale}
client={props.client}
size="compact"
chain={chain}
meta={{
title: props.locale.manageWallet.linkProfile,
showThirdwebBranding: false,
}}
isLinking={true}
goBack={props.onBack}
/>
</Suspense>
);
}
return (
<Container
style={{
@@ -1,6 +1,7 @@
"use client";
import { ShuffleIcon } from "@radix-ui/react-icons";
import type { ThirdwebClient } from "../../../../../client/client.js";
import { isEcosystemWallet } from "../../../../../wallets/ecosystem/is-ecosystem-wallet.js";
import { isInAppWallet } from "../../../../../wallets/in-app/core/wallet/index.js";
import { injectedProvider } from "../../../../../wallets/injected/mipdStore.js";
import { fontSize, iconSize } from "../../../../core/design-system/index.js";
@@ -56,21 +57,23 @@ export function ManageWalletScreen(props: {
/>
{/* Multi-auth */}
{activeWallet?.id === "inApp" && (
<MenuButton
onClick={() => {
props.setScreen("linked-profiles");
}}
style={{
fontSize: fontSize.sm,
}}
>
<MultiUserIcon size={iconSize.md} />
<Text color="primaryText">
{props.locale.manageWallet.linkedProfiles}
</Text>
</MenuButton>
)}
{activeWallet &&
(activeWallet?.id === "inApp" ||
isEcosystemWallet(activeWallet)) && (
<MenuButton
onClick={() => {
props.setScreen("linked-profiles");
}}
style={{
fontSize: fontSize.sm,
}}
>
<MultiUserIcon size={iconSize.md} />
<Text color="primaryText">
{props.locale.manageWallet.linkedProfiles}
</Text>
</MenuButton>
)}
{/* Wallet Connect Receiver */}
<MenuButton
@@ -47,12 +47,12 @@ function EcosystemWalletConnectUI(props: {
return <LoadingScreen />;
}
const goBackToMain =
props.size === "compact"
? props.goBack
: () => {
setSelectionData({});
};
const goBackToMain = () => {
if (props.size === "compact") {
props.goBack?.();
}
setSelectionData({});
};
const done = () => {
props.done();
@@ -1,20 +0,0 @@
"use client";
import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js";
import { fontSize } from "../../../core/design-system/index.js";
import { StyledButton } from "../../ui/design-system/elements.js";
export const LinkButton = /* @__PURE__ */ StyledButton((_) => {
const theme = useCustomTheme();
return {
all: "unset",
color: theme.colors.accentText,
fontSize: fontSize.sm,
cursor: "pointer",
textAlign: "center",
fontWeight: 500,
width: "100%",
"&:hover": {
color: theme.colors.primaryText,
},
};
});
@@ -266,7 +266,7 @@ export const ConnectWalletSocialOptions = (
const connectPromise = (() => {
if (props.isLinking) {
if (wallet.id !== "inApp") {
if (wallet.id !== "inApp" && !isEcosystemWallet(wallet)) {
throw new Error("Only in-app wallets support multi-auth");
}
return linkProfile(wallet, connectOptions);
@@ -1,5 +1,7 @@
import type { ThirdwebClient } from "../../../../client/client.js";
import { getThirdwebBaseUrl } from "../../../../utils/domains.js";
import { getClientFetch } from "../../../../utils/fetch.js";
import type { Ecosystem } from "../../web/types.js";
import type { Profile } from "./types.js";
/**
@@ -11,14 +13,17 @@ import type { Profile } from "./types.js";
*/
export async function linkAccount({
client,
ecosystem,
tokenToLink,
}: {
client: ThirdwebClient;
ecosystem?: Ecosystem;
tokenToLink: string;
}): Promise<Profile[]> {
const clientFetch = getClientFetch(client, ecosystem);
const IN_APP_URL = getThirdwebBaseUrl("inAppWallet");
const currentAccountToken = localStorage.getItem(
`walletToken-${client.clientId}`,
`walletToken-${client.clientId}${ecosystem?.id ? `-${ecosystem.id}` : ""}`,
);
if (!currentAccountToken) {
@@ -26,12 +31,10 @@ export async function linkAccount({
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer iaw-auth-token:${currentAccountToken}`,
"x-thirdweb-client-id": client.clientId,
"Content-Type": "application/json",
};
const linkedDetailsResp = await fetch(
const linkedDetailsResp = await clientFetch(
`${IN_APP_URL}/api/2024-05-05/account/connect`,
{
method: "POST",
@@ -61,23 +64,23 @@ export async function linkAccount({
*/
export async function getLinkedProfilesInternal({
client,
}: { client: ThirdwebClient }): Promise<Profile[]> {
ecosystem,
}: { client: ThirdwebClient; ecosystem?: Ecosystem }): Promise<Profile[]> {
const clientFetch = getClientFetch(client, ecosystem);
const IN_APP_URL = getThirdwebBaseUrl("inAppWallet");
const currentAccountToken = localStorage.getItem(
`walletToken-${client.clientId}`,
`walletToken-${client.clientId}${ecosystem?.id ? `-${ecosystem.id}` : ""}`,
);
const headers: Record<string, string> = {
Authorization: `Bearer iaw-auth-token:${currentAccountToken}`,
"Content-Type": "application/json",
};
if (!currentAccountToken) {
throw new Error("Failed to get linked accounts, no user logged in");
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer iaw-auth-token:${currentAccountToken}`,
"x-thirdweb-client-id": client.clientId,
};
const linkedAccountsResp = await fetch(
const linkedAccountsResp = await clientFetch(
`${IN_APP_URL}/api/2024-05-05/accounts`,
{
method: "GET",
@@ -8,6 +8,16 @@ import type {
CreateWalletArgs,
EcosystemWalletId,
} from "../../../wallet-types.js";
import type { Ecosystem } from "../../web/types.js";
import {
getLinkedProfilesInternal,
linkAccount as linkProfileWithToken,
} from "../authentication/linkAccount.js";
import type {
MultiStepAuthArgsType,
Profile,
SingleStepAuthArgsType,
} from "../authentication/types.js";
import type { InAppConnector } from "../interfaces/connector.js";
import { getOrCreateInAppWalletConnector } from "./in-app-core.js";
@@ -25,6 +35,10 @@ export function createEcosystemWallet(args: {
let account: Account | undefined = undefined;
let chain: Chain | undefined = undefined;
let client: ThirdwebClient | undefined;
const ecosystem: Ecosystem = {
id,
partnerId: createOptions?.partnerId,
};
return {
id,
@@ -38,7 +52,13 @@ export function createEcosystemWallet(args: {
return chain;
},
getConfig: () => createOptions,
getProfiles: async () => {
if (!client) {
return [];
}
return getLinkedProfilesInternal({ client, ecosystem });
},
getAccount: () => account,
autoConnect: async (options) => {
const { autoConnectInAppWallet } = await import("./index.js");
@@ -46,10 +66,7 @@ export function createEcosystemWallet(args: {
const connector = await getOrCreateInAppWalletConnector(
options.client,
connectorFactory,
{
id,
partnerId: createOptions?.partnerId,
},
ecosystem,
);
const [connectedAccount, connectedChain] = await autoConnectInAppWallet(
@@ -75,10 +92,7 @@ export function createEcosystemWallet(args: {
const connector = await getOrCreateInAppWalletConnector(
options.client,
connectorFactory,
{
id,
partnerId: createOptions?.partnerId,
},
ecosystem,
);
const [connectedAccount, connectedChain] = await connectInAppWallet(
@@ -104,10 +118,7 @@ export function createEcosystemWallet(args: {
const connector = await getOrCreateInAppWalletConnector(
client,
connectorFactory,
{
id,
partnerId: createOptions?.partnerId,
},
ecosystem,
);
const result = await connector.logout();
if (!result.success) {
@@ -122,5 +133,28 @@ export function createEcosystemWallet(args: {
chain = newChain;
emitter.emit("chainChanged", newChain);
},
};
// This is not included on the global interface but is force-resolved in linkProfile
linkProfile: async (
options: SingleStepAuthArgsType | MultiStepAuthArgsType,
): Promise<Profile[]> => {
if (!client) {
throw new Error(
"No client found, please connect the wallet before linking a profile",
);
}
const connector = await getOrCreateInAppWalletConnector(
client,
connectorFactory,
ecosystem,
);
const { storedToken } = await connector.authenticate(options);
return await linkProfileWithToken({
client,
ecosystem,
tokenToLink: storedToken.cookieString,
});
},
} as Wallet<EcosystemWalletId>;
}
@@ -1,3 +1,5 @@
import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js";
import type { EcosystemWalletId } from "../../../../wallets/wallet-types.js";
import type { Wallet } from "../../../interfaces/wallet.js";
import type {
MultiStepAuthArgsType,
@@ -25,8 +27,8 @@ import type {
* ```
* @wallet
*/
export async function getProfiles(wallet: Wallet<"inApp">) {
if (wallet.id !== "inApp") {
export async function getProfiles(wallet: Wallet<"inApp" | EcosystemWalletId>) {
if (wallet.id !== "inApp" && !isEcosystemWallet(wallet)) {
throw new Error("Multi-auth currently only supports in-app wallets");
}
@@ -59,10 +61,10 @@ export async function getProfiles(wallet: Wallet<"inApp">) {
* @wallet
*/
export async function linkProfile(
wallet: Wallet<"inApp">,
wallet: Wallet<"inApp" | EcosystemWalletId>,
auth: MultiStepAuthArgsType | SingleStepAuthArgsType,
): Promise<Profile[]> {
if (wallet.id !== "inApp") {
if (wallet.id !== "inApp" && !isEcosystemWallet(wallet)) {
throw new Error("Multi-auth currently only supports in-app wallets");
}