Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-boxes-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Support linking accounts with redirect mode
16 changes: 16 additions & 0 deletions apps/playground-web/src/components/in-app-wallet/profiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export function LinkAccount() {
});
};

const linkGithub = async () => {
linkProfile({
client: THIRDWEB_CLIENT,
strategy: "github",
mode: "redirect",
});
};

return (
<div className="flex flex-col gap-4 p-6">
{account ? (
Expand Down Expand Up @@ -83,6 +91,14 @@ export function LinkAccount() {
>
Link Passkey
</Button>
<Button
className="rounded-full p-6"
disabled={isPending}
onClick={linkGithub}
variant="default"
>
Link Github
</Button>
</>
)}
{error && <p className="text-red-500">Error: {error.message}</p>}
Expand Down
1 change: 1 addition & 0 deletions packages/thirdweb/src/exports/wallets/in-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export {
getUserEmail,
getUserPhoneNumber,
linkProfile,
linkProfileWithRedirect,
preAuthenticate,
unlinkProfile,
} from "../../wallets/in-app/web/lib/auth/index.js";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function useLinkProfile() {
onSuccess() {
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ["profiles"] });
}, 500);
}, 1000);
},
});
}
151 changes: 151 additions & 0 deletions packages/thirdweb/src/wallets/connection/autoConnectCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { timeoutPromise } from "../../utils/timeoutPromise.js";
import { isEcosystemWallet } from "../ecosystem/is-ecosystem-wallet.js";
import { ClientScopedStorage } from "../in-app/core/authentication/client-scoped-storage.js";
import { linkAccount } from "../in-app/core/authentication/linkAccount.js";
import type {
AuthArgsType,
AuthStoredTokenWithCookieReturnType,
Expand Down Expand Up @@ -83,6 +84,24 @@

const urlToken = getUrlToken();

// Handle linking flow: autoconnect with stored credentials, then link the new profile
if (urlToken?.authFlow === "link" && urlToken.authResult) {
const linkingResult = await handleLinkingFlow({
client: props.client,
connectOverride,
createWalletFn,
manager,
onConnect,
props,
setLastAuthProvider,
storage,
timeout,
urlToken,
wallets,
});
return linkingResult;
}

Check warning on line 103 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L89-L103

Added lines #L89 - L103 were not covered by tests

// If an auth cookie is found and this site supports the wallet, we'll set the auth cookie in the client storage
const wallet = wallets.find((w) => w.id === urlToken?.walletId);
if (urlToken?.authCookie && wallet) {
Expand Down Expand Up @@ -223,6 +242,138 @@
return autoConnected; // useQuery needs a return value
};

/**
* Handles the linking flow when returning from an OAuth redirect with authFlow=link.
* This autoconnects using stored credentials, then links the new profile from the URL token.
* @internal
*/
async function handleLinkingFlow(params: {

Check warning on line 250 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L250

Added line #L250 was not covered by tests
client: ThirdwebClient;
urlToken: NonNullable<ReturnType<typeof getUrlToken>>;
wallets: Wallet[];
storage: AsyncStorage;
manager: ConnectionManager;
onConnect?: (wallet: Wallet, connectedWallets: Wallet[]) => void;
timeout: number;
connectOverride?: (
walletOrFn: Wallet | (() => Promise<Wallet>),
) => Promise<Wallet | null>;
createWalletFn: (id: WalletId) => Wallet;
setLastAuthProvider?: (
authProvider: AuthArgsType["strategy"],
storage: AsyncStorage,
) => Promise<void>;
props: AutoConnectProps & { wallets: Wallet[] };
}): Promise<boolean> {
const {
client,
connectOverride,
createWalletFn,
manager,
onConnect,
props,
setLastAuthProvider,
storage,
timeout,
urlToken,
wallets,
} = params;

Check warning on line 280 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L267-L280

Added lines #L267 - L280 were not covered by tests

// Get stored wallet credentials (not from URL)
const [storedConnectedWalletIds, storedActiveWalletId] = await Promise.all([
getStoredConnectedWalletIds(storage),
getStoredActiveWalletId(storage),
]);
const lastConnectedChain =
(await getLastConnectedChain(storage)) || props.chain;

Check warning on line 288 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L283-L288

Added lines #L283 - L288 were not covered by tests

if (!storedActiveWalletId || !storedConnectedWalletIds) {
console.warn("No stored wallet found for linking flow");
manager.isAutoConnecting.setValue(false);
return false;
}

Check warning on line 294 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L290-L294

Added lines #L290 - L294 were not covered by tests

// Update auth provider if provided
if (urlToken.authProvider) {
await setLastAuthProvider?.(urlToken.authProvider, storage);
}

Check warning on line 299 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L297-L299

Added lines #L297 - L299 were not covered by tests

// Find or create the active wallet from stored credentials
const activeWallet =
wallets.find((w) => w.id === storedActiveWalletId) ||
createWalletFn(storedActiveWalletId);

Check warning on line 304 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L302-L304

Added lines #L302 - L304 were not covered by tests

// Autoconnect WITHOUT the URL token (use stored credentials)
manager.activeWalletConnectionStatusStore.setValue("connecting");
try {
await timeoutPromise(
handleWalletConnection({
authResult: undefined, // Don't use URL token for connection
client,
lastConnectedChain,
wallet: activeWallet,
}),
{
message: `AutoConnect timeout: ${timeout}ms limit exceeded.`,
ms: timeout,
},
);

Check warning on line 320 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L307-L320

Added lines #L307 - L320 were not covered by tests

await (connectOverride
? connectOverride(activeWallet)
: manager.connect(activeWallet, {
accountAbstraction: props.accountAbstraction,
client,
}));
} catch (e) {
console.warn("Failed to auto-connect for linking:", e);
manager.activeWalletConnectionStatusStore.setValue("disconnected");
manager.isAutoConnecting.setValue(false);
return false;
}

Check warning on line 333 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L322-L333

Added lines #L322 - L333 were not covered by tests

// Now link the new profile using URL auth token
const ecosystem = isEcosystemWallet(activeWallet)
? {
id: activeWallet.id,
partnerId: activeWallet.getConfig()?.partnerId,
}
: undefined;

Check warning on line 341 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L336-L341

Added lines #L336 - L341 were not covered by tests

const clientStorage = new ClientScopedStorage({
clientId: client.clientId,
ecosystem,
storage,
});

Check warning on line 347 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L343-L347

Added lines #L343 - L347 were not covered by tests

try {
await linkAccount({
client,
ecosystem,
storage: clientStorage,
tokenToLink: urlToken.authResult!.storedToken.cookieString,
});
} catch (e) {
console.error("Failed to link profile after redirect:", e);

Check warning on line 357 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L349-L357

Added lines #L349 - L357 were not covered by tests
// Continue - user is still connected, just linking failed
}

Check warning on line 359 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L359

Added line #L359 was not covered by tests

manager.isAutoConnecting.setValue(false);

Check warning on line 361 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L361

Added line #L361 was not covered by tests

const connectedWallet = manager.activeWalletStore.getValue();
const allConnectedWallets = manager.connectedWallets.getValue();
if (connectedWallet) {
try {
onConnect?.(connectedWallet, allConnectedWallets);
} catch (e) {
console.error("Error calling onConnect callback:", e);
}
return true;
}

Check warning on line 372 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L363-L372

Added lines #L363 - L372 were not covered by tests

return false;
}

Check warning on line 375 in packages/thirdweb/src/wallets/connection/autoConnectCore.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/connection/autoConnectCore.ts#L374-L375

Added lines #L374 - L375 were not covered by tests

/**
* @internal
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
ecosystem,
mode = "popup",
redirectUrl,
authFlow,
}: {
authOption: AuthOption;
client: ThirdwebClient;
ecosystem?: Ecosystem;
mode?: "popup" | "redirect" | "window";
redirectUrl?: string;
authFlow?: "connect" | "link";
}) => {
if (mode === "popup" && redirectUrl) {
throw new Error("Redirect URL is not supported for popup mode");
Expand All @@ -49,6 +51,9 @@
const formattedRedirectUrl = new URL(redirectUrl || window.location.href);
formattedRedirectUrl.searchParams.set("walletId", ecosystem?.id || "inApp");
formattedRedirectUrl.searchParams.set("authProvider", authOption);
if (authFlow) {
formattedRedirectUrl.searchParams.set("authFlow", authFlow);
}

Check warning on line 56 in packages/thirdweb/src/wallets/in-app/core/authentication/getLoginPath.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/core/authentication/getLoginPath.ts#L54-L56

Added lines #L54 - L56 were not covered by tests
baseUrl = `${baseUrl}&redirectUrl=${encodeURIComponent(formattedRedirectUrl.toString())}`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export interface InAppConnector {
mode?: "redirect" | "popup" | "window",
redirectUrl?: string,
): Promise<void>;
// Link a profile with redirect mode
linkProfileWithRedirect?(
strategy: SocialAuthOption,
mode?: "redirect" | "window",
redirectUrl?: string,
): Promise<void>;
// Login takes an auth token and connects a user with it
loginWithAuthToken?(
authResult: AuthStoredTokenWithCookieReturnType,
Expand Down
45 changes: 45 additions & 0 deletions packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,51 @@
);
}

/**
* Links a new profile to the current user using OAuth redirect flow.
*
* This function initiates a redirect-based OAuth flow for linking a social account.
* After the user completes authentication with the provider, they will be redirected
* back to your app. The `autoConnect` function will automatically detect the linking
* flow and complete the profile linking.
*
* @param args - The authentication arguments including strategy, client, and optional redirectUrl.
* @returns A promise that resolves when the redirect is initiated.
* @example
* ```ts
* import { linkProfileWithRedirect } from "thirdweb/wallets/in-app";
*
* await linkProfileWithRedirect({
* client,
* strategy: "google",
* mode: "redirect",
* redirectUrl: "https://example.org/callback",
* });
* // Browser will redirect to Google for authentication
* // After auth, user is redirected back and autoConnect handles the linking
* ```
* @wallet
*/
export async function linkProfileWithRedirect(
args: Omit<SocialAuthArgsType, "mode"> & {

Check warning on line 220 in packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts#L219-L220

Added lines #L219 - L220 were not covered by tests
client: ThirdwebClient;
ecosystem?: Ecosystem;
mode?: "redirect" | "window";
},
) {
const connector = await getInAppWalletConnector(args.client, args.ecosystem);
if (!connector.linkProfileWithRedirect) {
throw new Error(
"linkProfileWithRedirect is not supported on this platform",
);
}
return connector.linkProfileWithRedirect(
args.strategy as SocialAuthOption,
args.mode,
args.redirectUrl,
);
}

Check warning on line 237 in packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/auth/index.ts#L225-L237

Added lines #L225 - L237 were not covered by tests

/**
* Connects a new profile (and new authentication method) to the current user.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@
ecosystem?: Ecosystem;
redirectUrl?: string;
mode?: "redirect" | "popup" | "window";
authFlow?: "connect" | "link";
}): Promise<void> {
const loginUrl = getLoginUrl({
...options,
mode: options.mode || "redirect",
authFlow: options.authFlow,

Check warning on line 40 in packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/wallets/in-app/web/lib/auth/oauth.ts#L40

Added line #L40 was not covered by tests
});
if (options.mode === "redirect") {
window.location.href = loginUrl;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe.runIf(global.window !== undefined)("getUrlToken", () => {

expect(result).toEqual({
authCookie: null,
authFlow: null,
authProvider: null,
authResult: { token: "abc" },
walletId: "123",
Expand All @@ -62,6 +63,7 @@ describe.runIf(global.window !== undefined)("getUrlToken", () => {

expect(result).toEqual({
authCookie: "myCookie",
authFlow: null,
authProvider: null,
authResult: undefined,
walletId: "123",
Expand All @@ -79,6 +81,7 @@ describe.runIf(global.window !== undefined)("getUrlToken", () => {

expect(result).toEqual({
authCookie: "myCookie",
authFlow: null,
authProvider: "provider1",
authResult: { token: "xyz" },
walletId: "123",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function getUrlToken():
authResult?: AuthStoredTokenWithCookieReturnType;
authProvider?: AuthOption;
authCookie?: string;
authFlow?: "connect" | "link";
}
| undefined {
if (typeof document === "undefined") {
Expand All @@ -24,6 +25,7 @@ export function getUrlToken():
const walletId = params.get("walletId") as WalletId | undefined;
const authProvider = params.get("authProvider") as AuthOption | undefined;
const authCookie = params.get("authCookie") as string | undefined;
const authFlow = params.get("authFlow") as "connect" | "link" | undefined;

if ((authCookie || authResultString) && walletId) {
const authResult = (() => {
Expand All @@ -35,12 +37,13 @@ export function getUrlToken():
params.delete("walletId");
params.delete("authProvider");
params.delete("authCookie");
params.delete("authFlow");
window.history.pushState(
{},
"",
`${window.location.pathname}?${params.toString()}`,
);
return { authCookie, authProvider, authResult, walletId };
return { authCookie, authFlow, authProvider, authResult, walletId };
}
return undefined;
}
Loading
Loading