> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Linking accounts to users

Developers can use Privy to prompt users to link additional accounts (such as a wallet or Discord profile) at *any point* in their user journey, not just during login.

This is key to Privy's **progressive onboarding**: improving conversion and UX by requiring users to complete onboarding steps (e.g. connecting an account) only when necessary.

<Warning>
  **A user's Privy account and any associated embedded wallets become permanently inaccessible if
  the user loses their only login method.** Social OAuth providers (Twitter, Discord, etc.) can
  suspend or permanently delete accounts without notice — neither the app developer nor Privy can
  re-link a new authentication method on a user's behalf.

  Apps that hold onchain assets should prompt users to link at least one durable backup method
  (email, phone number, or passkey) in addition to social login.
</Warning>

<View title="React" icon="react">
  The React SDK supports linking all supported account types via our modal-guided link methods.
  **To prompt a user to link an account, use the respective method from the **`useLinkAccount`** hook:**

  | Method              | Description                                               | User Experience  |
  | ------------------- | --------------------------------------------------------- | ---------------- |
  | `linkEmail`         | Links email address                                       | Opens modal      |
  | `linkPhone`         | Links phone number                                        | Opens modal      |
  | `linkWallet`        | Links external wallet                                     | Opens modal      |
  | `linkGoogle`        | Links Google account                                      | Direct redirect  |
  | `linkApple`         | Links Apple account                                       | Direct redirect  |
  | `linkTwitter`       | Links Twitter account                                     | Direct redirect  |
  | `linkDiscord`       | Links Discord account                                     | Direct redirect  |
  | `linkGithub`        | Links Github account                                      | Direct redirect  |
  | `linkLinkedIn`      | Links LinkedIn account                                    | Direct redirect  |
  | `linkTiktok`        | Links TikTok account                                      | Direct redirect  |
  | `linkSpotify`       | Links Spotify account                                     | Direct redirect  |
  | `linkInstagram`     | Links Instagram account                                   | Direct redirect  |
  | `linkTelegram`      | Links Telegram account                                    | Direct redirect  |
  | `linkFarcaster`     | Links Farcaster account                                   | Displays QR code |
  | `linkPasskey`       | Links passkey                                             | Opens modal      |
  | `linkTwitch`        | Links Twitch account                                      | Direct redirect  |
  | `linkLine`          | Links LINE account                                        | Direct redirect  |
  | `linkOAuth`         | Links additional OAuth account                            | Direct redirect  |
  | `linkWithCustomJwt` | Links a custom JWT account (via `useLinkJwtAccount` hook) | Headless         |

  <Info>
    Users are only permitted to link **a single account** for a given account type, except for wallets and passkeys. Concretely, a user may link at most one email address, but can link as many wallets and passkeys as they'd like.
  </Info>

  <img src="https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/link-email.png?fit=max&auto=format&n=zlmLhiIqRR7ViKN0&q=85&s=7faa4c7332a637d835b9c04d48f92030" alt="Sample prompt to link a user's email after they have logged in" width="447" height="446" data-path="images/link-email.png" />

  Below is an example button for prompting a user to link an email to their account:

  ```tsx theme={"system"}
  import {useLinkAccount} from '@privy-io/react-auth';

  function LinkOptions() {
      const {linkEmail, linkGoogle, linkWallet} = useLinkAccount();

      return (
          <div className="link-options">
              <button onClick={linkEmail}>Link Email to user</button>
              <button onClick={linkGoogle}>Link Google account to user</button>
              <button onClick={linkWallet}>Link Wallet to user</button>
          </div>
      );
  }
  ```

  ### Callbacks

  You can optionally register an `onSuccess` or `onError` callback on the `useLinkAccount` hook.

  ```tsx theme={"system"}
  const {linkGoogle} = useLinkAccount({
      onSuccess: ({user, linkMethod, linkedAccount}) => {
          console.log('Linked account to user ', linkedAccount);
      },
      onError: (error) => {
          console.error('Failed to link account with error ', error)
      }
  })
  ```

  <ParamField path="onSuccess" type="({user: User, linkMethod: string, linkedAccount: linkedAccount}) => void">
    Optional callback to run after a user successfully links an account.
  </ParamField>

  <ParamField path="onError" type="(error: string) => void">
    Optional callback to run after there is an error during account linkage.
  </ParamField>

  <Info>
    **Looking for whitelabel `link` methods?** Our [`useLoginWith<AccountType>`](/authentication/user-authentication/login-methods/email) hooks allow will link an account to a user, provided that the user is already logged in whenever the authentication flow is completed. For headless wallet linking with `useLinkWithSiwe` or `useLinkWithSiws`, see the [whitelabel user management documentation](/user-management/users/whitelabel#react).
  </Info>

  ### Linking additional OAuth accounts

  The `linkOAuth` method allows your app to link [additional OAuth providers](/authentication/user-authentication/login-methods/custom-oauth) that are not natively supported by Privy. For built-in providers like Google or Twitter, use the dedicated methods (e.g., `linkGoogle`, `linkTwitter`).

  ```tsx theme={"system"}
  import {useLinkAccount} from '@privy-io/react-auth';

  function LinkOAuthButton() {
      const {linkOAuth} = useLinkAccount();

      return (
          <button onClick={() => linkOAuth({provider: 'custom:twitch'})}>
              Link Twitch account
          </button>
      );
  }
  ```

  #### Parameters

  The `linkOAuth` method accepts an object with the following fields:

  <ParamField path="provider" type="string" required>
    The additional OAuth provider to link, in the format `'custom:<provider-name>'` (e.g., `'custom:twitch'`).
  </ParamField>

  ### Linking custom JWT accounts

  If your app uses an [external JWT-based authentication provider](/authentication/user-authentication/jwt-based-auth/setup), use the `useLinkJwtAccount` hook to link a custom JWT account to an already-authenticated Privy user. Unlike the methods above, this hook is headless: you provide the JWT yourself and Privy verifies it on the server.

  ```tsx theme={"system"}
  import {useLinkJwtAccount} from '@privy-io/react-auth';

  function LinkJwtAccountButton() {
      const {linkWithCustomJwt, state} = useLinkJwtAccount();

      const handleLink = async () => {
          const jwt = await getJwtFromExternalAuth();
          await linkWithCustomJwt(jwt);
      };

      return (
          <button onClick={handleLink} disabled={state.status === 'loading'}>
              Link external account
          </button>
      );
  }
  ```

  #### Parameters

  <ParamField path="jwt" type="string" required>
    The JWT issued by your external authentication provider to link to the user's account.
  </ParamField>

  #### State

  The `state` property tracks the current state of the JWT linking flow:

  | Status          | Description                                 |
  | --------------- | ------------------------------------------- |
  | `'initial'`     | The flow has not started                    |
  | `'loading'`     | The JWT is being verified and linked        |
  | `'not-enabled'` | Custom JWT auth is not enabled for this app |
  | `'done'`        | The account was linked successfully         |
  | `'error'`       | An error occurred                           |

  #### Callbacks

  You can optionally pass `onSuccess` and `onError` callbacks into `useLinkJwtAccount`:

  ```tsx theme={"system"}
  const {linkWithCustomJwt, state} = useLinkJwtAccount({
      onSuccess: ({user, linkMethod, linkedAccount}) => {
          console.log('JWT account linked successfully', linkedAccount);
      },
      onError: (error) => {
          console.error('Failed to link JWT account', error);
      }
  });
  ```

  <Note>
    Custom JWT authentication must be enabled in your Privy Dashboard before using this hook. See the [JWT-based authentication setup docs](/authentication/user-authentication/jwt-based-auth/setup) for instructions.
  </Note>
</View>

<View title="React Native" icon="react">
  **To prompt a user to link an account, use the respective hooks:**

  | Account type | Description             | Hook to invoke                                     |
  | ------------ | ----------------------- | -------------------------------------------------- |
  | `Email`      | Links email address     | `useLinkEmail`                                     |
  | `Phone`      | Links phone number      | `useLinkSMS`                                       |
  | `Wallet`     | Links external wallet   | `useLinkWithSiwe`, `useLinkWithSiws`               |
  | `Google`     | Links Google account    | `useLinkWithOAuth`                                 |
  | `Apple`      | Links Apple account     | `useLinkWithOAuth`                                 |
  | `Twitter`    | Links Twitter account   | `useLinkWithOAuth`                                 |
  | `Discord`    | Links Discord account   | `useLinkWithOAuth`                                 |
  | `Github`     | Links Github account    | `useLinkWithOAuth`                                 |
  | `LinkedIn`   | Links LinkedIn account  | `useLinkWithOAuth`                                 |
  | `TikTok`     | Links TikTok account    | `useLinkWithOAuth`                                 |
  | `Spotify`    | Links Spotify account   | `useLinkWithOAuth`                                 |
  | `Instagram`  | Links Instagram account | `useLinkWithOAuth`                                 |
  | `Farcaster`  | Links Farcaster account | `useLinkWithFarcaster`                             |
  | `Passkey`    | Links passkey           | `useLinkWithPasskey` from `@privy-io/expo/passkey` |

  <Info>
    Users are only permitted to link **a single account** for a given account type, except for wallets and passkeys. Concretely, a user may link at most one email address, but can link as many wallets and passkeys as they'd like.
  </Info>

  <Tabs>
    <Tab title="Email">
      Use the `useLinkEmail` hook to link an email address to an existing user.

      ```tsx theme={"system"}
      import {useLinkEmail} from '@privy-io/expo';

      const {sendCode, linkWithCode, state} = useLinkEmail();
      ```

      ### Send code

      ```tsx theme={"system"}
      sendCode({email: string}) => Promise<{success: boolean}>
      ```

      <ParamField path="email" type="string" required>
        The email address to send the verification code to.
      </ParamField>

      ### Link with code

      ```tsx theme={"system"}
      linkWithCode({code: string, email?: string}) => Promise<User | undefined>
      ```

      <ParamField path="code" type="string" required>
        The one-time passcode received at the email address.
      </ParamField>

      <ParamField path="email" type="string">
        Optional. The email address to link. If omitted, uses the email from `sendCode`.
      </ParamField>

      ### State

      The `state` property tracks the current state of the OTP flow:

      | Status                  | Description                            |
      | ----------------------- | -------------------------------------- |
      | `'initial'`             | The flow has not started               |
      | `'sending-code'`        | The code is being sent                 |
      | `'awaiting-code-input'` | Waiting for the user to enter the code |
      | `'submitting-code'`     | The code is being verified             |
      | `'done'`                | The email was linked successfully      |
      | `'error'`               | An error occurred                      |

      ### Callbacks

      ```tsx theme={"system"}
      const {sendCode, linkWithCode, state} = useLinkEmail({
        onSendCodeSuccess: ({email}) => console.log('Code sent to', email),
        onLinkSuccess: ({user}) => console.log('Email linked', user),
        onError: (error) => console.error(error),
      });
      ```

      ### Usage

      ```tsx theme={"system"}
      import {useLinkEmail} from '@privy-io/expo';

      function LinkEmailButton() {
        const {sendCode, linkWithCode, state} = useLinkEmail({
          onLinkSuccess: ({user}) => console.log('Email linked!', user),
          onError: (error) => console.error('Link failed:', error),
        });

        const handleSendCode = async () => {
          await sendCode({email: 'user@example.com'});
        };

        const handleVerify = async (code: string) => {
          await linkWithCode({code});
        };

        return (
          <View>
            <Button onPress={handleSendCode} title="Send Code" />
            <TextInput onSubmitEditing={(e) => handleVerify(e.nativeEvent.text)} />
          </View>
        );
      }
      ```
    </Tab>

    <Tab title="SMS">
      Use the `useLinkSMS` hook to link a phone number to an existing user.

      ```tsx theme={"system"}
      import {useLinkSMS} from '@privy-io/expo';

      const {sendCode, linkWithCode, state} = useLinkSMS();
      ```

      ### Send code

      ```tsx theme={"system"}
      sendCode({phone: string}) => Promise<{success: boolean}>
      ```

      <ParamField path="phone" type="string" required>
        The phone number to send the verification code to (e.g., `'+1234567890'`).
      </ParamField>

      ### Link with code

      ```tsx theme={"system"}
      linkWithCode({code: string, phone?: string}) => Promise<User | undefined>
      ```

      <ParamField path="code" type="string" required>
        The one-time passcode received at the phone number.
      </ParamField>

      <ParamField path="phone" type="string">
        Optional. The phone number to link. If omitted, uses the phone from `sendCode`.
      </ParamField>

      ### State

      The `state` property tracks the current state of the OTP flow (same states as email).

      ### Callbacks

      ```tsx theme={"system"}
      const {sendCode, linkWithCode, state} = useLinkSMS({
        onSendCodeSuccess: ({phone}) => console.log('Code sent to', phone),
        onLinkSuccess: ({user}) => console.log('Phone linked', user),
        onError: (error) => console.error(error),
      });
      ```

      ### Usage

      ```tsx theme={"system"}
      import {useLinkSMS} from '@privy-io/expo';

      function LinkPhoneButton() {
        const {sendCode, linkWithCode, state} = useLinkSMS({
          onLinkSuccess: ({user}) => console.log('Phone linked!', user),
          onError: (error) => console.error('Link failed:', error),
        });

        const handleSendCode = async () => {
          await sendCode({phone: '+1234567890'});
        };

        const handleVerify = async (code: string) => {
          await linkWithCode({code});
        };

        return (
          <View>
            <Button onPress={handleSendCode} title="Send Code" />
            <TextInput onSubmitEditing={(e) => handleVerify(e.nativeEvent.text)} />
          </View>
        );
      }
      ```
    </Tab>

    <Tab title="OAuth">
      Use the `useLinkWithOAuth` hook to link social accounts (Google, Apple, Twitter, Discord, etc.) to an existing user.

      ```tsx theme={"system"}
      import {useLinkWithOAuth} from '@privy-io/expo';

      const {link, state} = useLinkWithOAuth();
      ```

      ### Link

      ```tsx theme={"system"}
      link({provider: OAuthProviderID, redirectUri?: string}) => Promise<User | undefined>
      ```

      <ParamField path="provider" type="OAuthProviderID" required>
        The OAuth provider to link. Supported values: `'google'`, `'apple'`, `'twitter'`, `'discord'`, `'github'`, `'linkedin'`, `'tiktok'`, `'spotify'`, `'instagram'`.
      </ParamField>

      <ParamField path="redirectUri" type="string">
        Optional. A URL path that the provider will redirect to after authentication.
      </ParamField>

      ### State

      The `state` property tracks the current state of the OAuth flow:

      | Status      | Description                         |
      | ----------- | ----------------------------------- |
      | `'initial'` | The flow has not started            |
      | `'loading'` | The OAuth flow is in progress       |
      | `'done'`    | The account was linked successfully |
      | `'error'`   | An error occurred                   |

      ### Usage

      ```tsx theme={"system"}
      import {useLinkWithOAuth} from '@privy-io/expo';

      function LinkSocialButtons() {
        const {link, state} = useLinkWithOAuth();

        return (
          <View>
            <Button
              onPress={() => link({provider: 'google'})}
              title="Link Google"
              disabled={state.status === 'loading'}
            />
            <Button
              onPress={() => link({provider: 'discord'})}
              title="Link Discord"
              disabled={state.status === 'loading'}
            />
            <Button
              onPress={() => link({provider: 'twitter'})}
              title="Link Twitter"
              disabled={state.status === 'loading'}
            />
          </View>
        );
      }
      ```
    </Tab>

    <Tab title="Farcaster">
      Use the `useLinkWithFarcaster` hook to link a Farcaster account to an existing user.

      ```tsx theme={"system"}
      import {useLinkWithFarcaster} from '@privy-io/expo';

      const {linkWithFarcaster, cancel, state} = useLinkWithFarcaster();
      ```

      ### Link with Farcaster

      ```tsx theme={"system"}
      linkWithFarcaster(input?: {relyingParty?: string, redirectUrl?: string}) => Promise<User>
      ```

      <ParamField path="relyingParty" type="string">
        Optional. The relying party for the Farcaster authentication.
      </ParamField>

      <ParamField path="redirectUrl" type="string">
        Optional. The URL to redirect to after authentication.
      </ParamField>

      ### Cancel

      ```tsx theme={"system"}
      cancel() => void
      ```

      Cancels an in-progress Farcaster linking flow.

      ### State

      The `state` property tracks the current state of the Farcaster flow:

      | Status             | Description                           |
      | ------------------ | ------------------------------------- |
      | `'initial'`        | The flow has not started              |
      | `'loading'`        | The flow is in progress               |
      | `'awaiting-nonce'` | Waiting for nonce generation          |
      | `'polling'`        | Polling for authentication completion |
      | `'done'`           | The account was linked successfully   |
      | `'error'`          | An error occurred                     |

      ### Callbacks

      ```tsx theme={"system"}
      const {linkWithFarcaster, cancel, state} = useLinkWithFarcaster({
        onSuccess: ({user, linkedAccount}) => console.log('Farcaster linked', linkedAccount),
        onError: (error) => console.error(error),
      });
      ```

      ### Usage

      ```tsx theme={"system"}
      import {useLinkWithFarcaster} from '@privy-io/expo';

      function LinkFarcasterButton() {
        const {linkWithFarcaster, cancel, state} = useLinkWithFarcaster({
          onSuccess: ({user}) => console.log('Farcaster linked!', user),
          onError: (error) => console.error('Link failed:', error),
        });

        if (state.status === 'polling') {
          return (
            <View>
              <Text>Complete authentication in Warpcast...</Text>
              <Button onPress={cancel} title="Cancel" />
            </View>
          );
        }

        return (
          <Button
            onPress={() => linkWithFarcaster()}
            title="Link Farcaster"
            disabled={state.status === 'loading'}
          />
        );
      }
      ```
    </Tab>

    <Tab title="Wallet (SIWE)">
      Use the `useLinkWithSiwe` hook to link an Ethereum wallet to an existing user via Sign-In with Ethereum.

      ```tsx theme={"system"}
      import {useLinkWithSiwe} from '@privy-io/expo';

      const {generateSiweMessage, linkWithSiwe, state} = useLinkWithSiwe();
      ```

      ### Generate SIWE message

      ```tsx theme={"system"}
      generateSiweMessage({
        wallet: ExternalWallet,
        from: {domain: string, uri: string}
      }) => Promise<string>
      ```

      <ParamField path="wallet" type="ExternalWallet" required>
        The external wallet object to generate the message for.
      </ParamField>

      <ParamField path="from.domain" type="string" required>
        Your app's domain (e.g., `'myapp.com'`).
      </ParamField>

      <ParamField path="from.uri" type="string" required>
        Your app's URI (e.g., `'https://myapp.com'`).
      </ParamField>

      ### Link with SIWE

      ```tsx theme={"system"}
      linkWithSiwe({
        signature: string,
        messageOverride?: string
      }) => Promise<User>
      ```

      <ParamField path="signature" type="string" required>
        The signature from the wallet signing the SIWE message.
      </ParamField>

      <ParamField path="messageOverride" type="string">
        Optional. Override the message if different from the generated one.
      </ParamField>

      ### State

      The `state` property tracks the current state of the SIWE flow:

      | Status                   | Description                        |
      | ------------------------ | ---------------------------------- |
      | `'initial'`              | The flow has not started           |
      | `'generating-message'`   | Generating the SIWE message        |
      | `'awaiting-signature'`   | Waiting for wallet signature       |
      | `'submitting-signature'` | Submitting the signature           |
      | `'done'`                 | The wallet was linked successfully |
      | `'error'`                | An error occurred                  |

      ### Callbacks

      ```tsx theme={"system"}
      const {generateSiweMessage, linkWithSiwe, state} = useLinkWithSiwe({
        onSuccess: ({user, linkedAccount}) => console.log('Wallet linked', linkedAccount),
        onError: (error) => console.error(error),
        onGenerateMessage: () => console.log('Generating message...'),
      });
      ```

      ### Usage

      ```tsx theme={"system"}
      import {useLinkWithSiwe} from '@privy-io/expo';

      function LinkWalletButton({wallet}) {
        const {generateSiweMessage, linkWithSiwe, state} = useLinkWithSiwe({
          onSuccess: ({user}) => console.log('Wallet linked!', user),
          onError: (error) => console.error('Link failed:', error),
        });

        const handleLink = async () => {
          const message = await generateSiweMessage({
            wallet,
            from: {domain: 'myapp.com', uri: 'https://myapp.com'},
          });

          const signature = await wallet.sign(message);

          await linkWithSiwe({signature});
        };

        return (
          <Button
            onPress={handleLink}
            title="Link Wallet"
            disabled={state.status !== 'initial'}
          />
        );
      }
      ```
    </Tab>

    <Tab title="Wallet (SIWS)">
      Use the `useLinkWithSiws` hook to link a Solana wallet to an existing user via Sign-In with Solana.

      ```tsx theme={"system"}
      import {useLinkWithSiws} from '@privy-io/expo';

      const {generateMessage, link} = useLinkWithSiws();
      ```

      ### Generate SIWS message

      ```tsx theme={"system"}
      generateMessage({
        wallet: {address: string},
        from: {domain: string, uri: string}
      }) => Promise<{message: string}>
      ```

      <ParamField path="wallet.address" type="string" required>
        The Solana wallet address.
      </ParamField>

      <ParamField path="from.domain" type="string" required>
        Your app's domain (e.g., `'myapp.com'`).
      </ParamField>

      <ParamField path="from.uri" type="string" required>
        Your app's URI (e.g., `'https://myapp.com'`).
      </ParamField>

      ### Link

      ```tsx theme={"system"}
      link({
        signature: string,
        message: string,
        wallet?: {walletClientType?: string, connectorType?: string}
      }) => Promise<User>
      ```

      <ParamField path="signature" type="string" required>
        The signature from the wallet signing the SIWS message.
      </ParamField>

      <ParamField path="message" type="string" required>
        The SIWS message that was signed.
      </ParamField>

      <ParamField path="wallet.walletClientType" type="string">
        Optional. The wallet client type (e.g., `'phantom'`).
      </ParamField>

      <ParamField path="wallet.connectorType" type="string">
        Optional. The connector type used.
      </ParamField>

      ### Usage

      ```tsx theme={"system"}
      import {useLinkWithSiws} from '@privy-io/expo';

      function LinkSolanaWalletButton({wallet}) {
        const {generateMessage, link} = useLinkWithSiws();

        const handleLink = async () => {
          const {message} = await generateMessage({
            wallet: {address: wallet.address},
            from: {domain: 'myapp.com', uri: 'https://myapp.com'},
          });

          const signature = await wallet.signMessage(message);

          await link({
            signature,
            message,
            wallet: {walletClientType: 'phantom'},
          });
        };

        return <Button onPress={handleLink} title="Link Solana Wallet" />;
      }
      ```
    </Tab>

    <Tab title="Passkey">
      Use the `useLinkWithPasskey` hook from `@privy-io/expo/passkey` to link a passkey to an existing user.

      ```tsx theme={"system"}
      import {useLinkWithPasskey} from '@privy-io/expo/passkey';

      const {linkWithPasskey, state} = useLinkWithPasskey();
      ```

      ### Link with passkey

      ```tsx theme={"system"}
      linkWithPasskey({relyingParty: string}) => Promise<User | undefined>
      ```

      <ParamField path="relyingParty" type="string" required>
        The relying party identifier for the passkey (typically your app's domain).
      </ParamField>

      ### State

      The `state` property tracks the current state of the passkey flow:

      | Status                   | Description                         |
      | ------------------------ | ----------------------------------- |
      | `'initial'`              | The flow has not started            |
      | `'generating-challenge'` | Generating the passkey challenge    |
      | `'awaiting-passkey'`     | Waiting for passkey creation        |
      | `'submitting-response'`  | Submitting the passkey response     |
      | `'done'`                 | The passkey was linked successfully |
      | `'error'`                | An error occurred                   |

      ### Callbacks

      ```tsx theme={"system"}
      const {linkWithPasskey, state} = useLinkWithPasskey({
        onSuccess: ({user, linkedAccount}) => console.log('Passkey linked', linkedAccount),
        onError: (error) => console.error(error),
      });
      ```

      ### Usage

      ```tsx theme={"system"}
      import {useLinkWithPasskey} from '@privy-io/expo/passkey';

      function LinkPasskeyButton() {
        const {linkWithPasskey, state} = useLinkWithPasskey({
          onSuccess: ({user}) => console.log('Passkey linked!', user),
          onError: (error) => console.error('Link failed:', error),
        });

        return (
          <Button
            onPress={() => linkWithPasskey({relyingParty: 'myapp.com'})}
            title="Link Passkey"
            disabled={state.status !== 'initial'}
          />
        );
      }
      ```
    </Tab>

    <Tab title="Cross-App">
      Use the `useLinkWithCrossApp` hook to link a cross-app account (global wallet) to an existing user.

      ```tsx theme={"system"}
      import {useLinkWithCrossApp} from '@privy-io/expo';

      const {linkWithCrossApp} = useLinkWithCrossApp();
      ```

      ### Link with cross-app

      ```tsx theme={"system"}
      linkWithCrossApp({appId: string, redirectUri?: string}) => Promise<{user: User}>
      ```

      <ParamField path="appId" type="string" required>
        The Privy app ID of the provider app from which to link the account.
      </ParamField>

      <ParamField path="redirectUri" type="string">
        Optional. A URL path to redirect to after authentication.
      </ParamField>

      ### Usage

      ```tsx theme={"system"}
      import {useLinkWithCrossApp} from '@privy-io/expo';

      function LinkCrossAppButton() {
        const {linkWithCrossApp} = useLinkWithCrossApp();

        const handleLink = async () => {
          const {user} = await linkWithCrossApp({
            appId: 'provider-app-id',
          });
          console.log('Cross-app account linked!', user);
        };

        return <Button onPress={handleLink} title="Link Global Wallet" />;
      }
      ```
    </Tab>
  </Tabs>
</View>

<View title="Android" icon="android">
  <Tabs>
    <Tab title="Email/Phone">
      The steps to implementing the `linkWithCode` flow are analogous to the [`loginWithCode` flow](/authentication/user-authentication/login-methods/email). The same thing applies for the `PhoneAccount` type, using `privy.sms.linkWithCode` similar to how [`privy.sms.loginWithCode`](/authentication/user-authentication/login-methods/sms) works.

      First, prompt the user for their email address and use the Privy client's `privy.email.sendCode` method to send them a one-time passcode:

      ```kotlin theme={"system"}
      sendCode(email: String): Result<Unit>
      ```

      ### Link With Code

      Then, prompt the user for the code they received and use the `linkWithCode` method:

      ```kotlin theme={"system"}
      linkWithCode(code: String, email: String? = null): Result<PrivyUser>
      ```

      <ParamField path="code" type="String" required={true}>
        The one-time passcode sent to the user's email address.
      </ParamField>

      <ParamField path="email" type="String" required={false}>
        (Optional)  The user's email address. Though this parameter is optional, it is highly recommended that you pass the user's email address explicitly. If email is omitted, the email from `sendCode` will be used.
      </ParamField>

      ### Returns

      <ResponseField name="Result<PrivyUser>" type="Result<PrivyUser>">
        A result type encapsulating the PrivyUser on success.
      </ResponseField>

      ## Usage

      ```kotlin theme={"system"}
      // Send code to user's email
      val sendResult: Result<Unit> = privy.email.sendCode(email = "user@test.com")

      sendResult.fold(
          onSuccess = {
              // OTP was successfully sent, now prompt user for code
          },
          onFailure = {
              println("Error sending OTP: ${it.message}")
          }
      )

      // Link the email using the code
      val linkResult: Result<PrivyUser> = privy.email.linkWithCode(code = "123456", email = "user@test.com")

      linkResult.fold(
          onSuccess = { updatedUser ->
              println("Email successfully linked!")
              // updatedUser contains the updated user object with the email added
          },
          onFailure = {
              println("Error linking email: ${it.message}")
          }
      )
      ```
    </Tab>

    <Tab title="External Wallets">
      The steps to implementing the `link` flow are analogous to the [login flow's](/authentication/user-authentication/login-methods/wallet#android) of both Sign in with Ethereum (SIWE) and Sign in with Solana (SIWS).

      <Tabs>
        <Tab title="Ethereum Wallet">
          To link an Ethereum wallet, use the Privy client's `siwe` handler.

          ## Generate SIWE message

          ```kotlin theme={"system"}
          public suspend fun generateMessage(params: SiweMessageParams): Result<String>
          ```

          ### Parameters

          <ParamField path="params" type="SiweMessageParams">
            Set of parameters required to generate the message.

            <Expandable defaultOpen="true">
              <ParamField path="appDomain" type="String" required>
                Your app's domain. e.g. "my-domain.com"
              </ParamField>

              <ParamField path="appUri" type="String" required>
                Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)"
              </ParamField>

              <ParamField path="chainId" type="String" required>
                EVM Chain ID, e.g. "1" for Ethereum Mainnet
              </ParamField>

              <ParamField path="walletAddress" type="String" required>
                The user's [ERC-55](https://eips.ethereum.org/EIPS/eip-55) compliant wallet address.
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="String" type="Result<String>">
            A result type encapsulating the SIWE message that can be signed by the wallet on success.
          </ResponseField>

          ### Usage

          ```kotlin theme={"system"}
          val params = SiweMessageParams(
              appDomain = domain,
              appUri = uri,
              chainId = chainId,
              walletAddress = walletAddress
          )

          privy.siwe.generateSiweMessage(params = params).fold(
              onSuccess = { message ->
                  // request an EIP-191 `personal_sign` signature on the message
              },
              onFailure = { e ->
                  // An error can be thrown if the network call to generate the message fails,
                  // or if invalid metadata was passed in.
              }
          )
          ```

          ## Sign the SIWE message

          Using the message returned by `generateMessage`, request an EIP-191 `personal_sign` signature from the user's connected wallet. You should do this using the library your app uses to connect to external wallets (e.g. the MetaMask SDK or WalletConnect). Once the user successfully signs the message, pass the signature into the `link` function.

          ## Link with SIWE

          ```kotlin theme={"system"}
          public suspend fun link(
              message: String,
              signature: String,
              params: SiweMessageParams,
              metadata: WalletLoginMetadata?,
          ): Result<PrivyUser>
          ```

          ### Parameters

          <ParamField path="message" type="String" required>
            The message returned from "generateMessage".
          </ParamField>

          <ParamField path="signature" type="String" required>
            The signature of the SIWE message, signed by the user's wallet.
          </ParamField>

          <ParamField path="params" type="SiweMessageParams" required>
            The same SiweMessageParams passed into "generateMessage".
          </ParamField>

          <ParamField path="metadata" type="WalletLoginMetadata">
            (Optional) you can pass additional metadata that will be stored with the linked wallet.

            <Expandable defaultOpen="true">
              <ParamField path="walletClientType" type="WalletClientType">
                An enum specifying the type of wallet used to link. e.g. WalletClientType.Metamask
              </ParamField>

              <ParamField path="connectorType" type="String">
                A string identifying how wallet was connected. e.g. "wallet\_connect"
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="Result<PrivyUser>" type="Result<PrivyUser>">
            A result type encapsulating the PrivyUser on success.
          </ResponseField>

          ### Usage

          ```kotlin theme={"system"}
          val params = SiweMessageParams(
              appDomain = domain,
              appUri = uri,
              chainId = chainId,
              walletAddress = walletAddress
          )

          // optional metadata
          val metadata = WalletLoginMetadata(walletClientType = walletClient, connectorType = connectorType)

          privy.siwe.link(message, signature, params, metadata).fold(
              onSuccess = { updatedUser ->
                  // Link success
                  // updatedUser contains the updated user object with the wallet added
              },
              onFailure = { e ->
                  // Link failure, either due to invalid signature or network error
              }
          )
          ```
        </Tab>

        <Tab title="Solana Wallet">
          To link a Solana wallet, use the Privy client's `siws` handler.

          ## Generate SIWS message

          ```kotlin theme={"system"}
          public suspend fun generateMessage(params: SiwsMessageParams): Result<String>
          ```

          ### Parameters

          <ParamField path="params" type="SiwsMessageParams">
            Set of parameters required to generate the message.

            <Expandable defaultOpen="true">
              <ParamField path="appDomain" type="String" required>
                Your app's domain. e.g. "my-domain.com"
              </ParamField>

              <ParamField path="appUri" type="String" required>
                Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)"
              </ParamField>

              <ParamField path="walletAddress" type="String" required>
                The user's Solana wallet address.
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="String" type="Result<String>">
            A result type encapsulating the SIWS message that can be signed by the wallet on success.
          </ResponseField>

          ### Usage

          ```kotlin theme={"system"}
          val params = SiwsMessageParams(
              appDomain = domain,
              appUri = uri,
              walletAddress = walletAddress
          )

          privy.siws.generateMessage(params = params).fold(
              onSuccess = { message ->
                  // request a Solana wallet signature on the message
              },
              onFailure = { e ->
                  // An error can be thrown if the network call to generate the message fails,
                  // or if invalid metadata was passed in.
              }
          )
          ```

          ## Sign the SIWS message

          Using the message returned by `generateMessage`, request a signature from the user's connected Solana wallet. You should do this using the library your app uses to connect to external wallets (e.g. the Solana Mobile SDK or Phantom). Once the user successfully signs the message, pass the signature into the `link` function.

          ## Link with SIWS

          ```kotlin theme={"system"}
          public suspend fun link(
              message: String,
              signature: String,
              params: SiwsMessageParams,
              metadata: WalletLoginMetadata?,
          ): Result<PrivyUser>
          ```

          ### Parameters

          <ParamField path="message" type="String" required>
            The message returned from "generateMessage".
          </ParamField>

          <ParamField path="signature" type="String" required>
            The signature of the SIWS message, signed by the user's wallet.
          </ParamField>

          <ParamField path="params" type="SiwsMessageParams" required>
            The same SiwsMessageParams passed into "generateMessage".
          </ParamField>

          <ParamField path="metadata" type="WalletLoginMetadata">
            (Optional) you can pass additional metadata that will be stored with the linked wallet.

            <Expandable defaultOpen="true">
              <ParamField path="walletClientType" type="String">
                The client of the connected wallet (e.g. "phantom").
              </ParamField>

              <ParamField path="connectorType" type="String">
                A string identifying how wallet was connected (e.g. "wallet\_connect" or "mobile\_wallet\_protocol").
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="Result<PrivyUser>" type="Result<PrivyUser>">
            A result type encapsulating the PrivyUser on success.
          </ResponseField>

          ### Usage

          ```kotlin theme={"system"}
          val params = SiwsMessageParams(
              appDomain = domain,
              appUri = uri,
              walletAddress = walletAddress
          )

          // optional metadata
          val metadata = WalletLoginMetadata(
              walletClientType = walletClient,
              connectorType = connectorType
          )

          privy.siws.link(message, signature, params, metadata).fold(
              onSuccess = { updatedUser ->
                  // Link success
                  // updatedUser contains the updated user object with the wallet added
              },
              onFailure = { e ->
                  // Link failure, either due to invalid signature or network error
              }
          )
          ```
        </Tab>
      </Tabs>
    </Tab>

    <Tab title="Passkeys">
      Use the following method from the `passkey` handler to link a new passkey to a user. Be sure to follow Android passkey setup instructions [here](/authentication/user-authentication/login-methods/passkey#android).

      ```kotlin theme={"system"}
      suspend fun link(
          relyingParty: String,
          displayName: String? = null
      ): Result<PrivyUser>
      ```

      ### Parameters

      <ParamField path="relyingParty" type="String" required>
        The URL origin where your Digital Asset Links are available (e.g., `https://example.com`).
      </ParamField>

      <ParamField path="displayName" type="String">
        An optional display name to associate with the passkey. This name will be shown to the user when selecting which passkey to use for authentication.
      </ParamField>

      ### Returns

      <ResponseField name="Result<PrivyUser>" type="Result<PrivyUser>">
        A result type encapsulating the PrivyUser on success.
      </ResponseField>

      ## Usage

      ```kotlin theme={"system"}
      val relyingParty = "https://yourdomain.com"
      val displayName = "Optional Display Name" // Optional parameter

      privy.passkey.link(
          relyingParty = relyingParty,
          displayName = displayName
      ).fold(
          onSuccess = { updatedUser ->
              // Successfully linked a passkey to an existing user
              // updatedUser contains the updated user object with the passkey added
              println("Passkey linked successfully!")
          },
          onFailure = { error ->
              // Handle error
              println("Link failed: ${error.message}")
          }
      )
      ```
    </Tab>

    <Tab title="OAuth">
      Use the following method from the OAuth handler to link a new OAuth account to an existing user:

      ```kotlin theme={"system"}
      suspend fun link(oAuthProvider: OAuthProvider, appUrlScheme: String): Result<PrivyUser>
      ```

      ### Returns

      <ResponseField name="Result<PrivyUser>" type="Result<PrivyUser>">
        A Result containing the updated user object with the newly linked OAuth account on success, or an exception on failure.
      </ResponseField>

      ## Usage

      ```kotlin theme={"system"}
      val scheme = "your-app-scheme://"

      privy.oAuth
          .link(OAuthProvider.Google, scheme)
          .fold(
              onSuccess = { user ->
                  // Successfully linked an OAuth account to an existing user
              },
              onFailure = { error ->
                  println("Linking failed: ${error.message}")
              }
          )
      ```
    </Tab>
  </Tabs>
</View>

<View title="Swift" icon="swift">
  <Tabs>
    <Tab title="Email/Phone">
      The steps to implementing the `linkWithCode` flow are analogous to the [`loginWithCode` flow](/authentication/user-authentication/login-methods/email).
      The same thing applies for the `PhoneNumberAccount` type, using `privy.sms.linkWithCode` similar to how [`privy.sms.loginWithCode`](/authentication/user-authentication/login-methods/sms) works.

      First, prompt the user for their email address and use the Privy client's `privy.email.sendCode` method to send them a one-time passcode:

      ```swift theme={"system"}
      sendCode(to email: String) async throws
      ```

      ### Link With Code

      Then, prompt the user for the code they received and use the `linkWithCode` method:

      ```swift theme={"system"}
      linkWithCode(_ code: String, sentTo email: String) async throws -> PrivyUser
      ```

      <ParamField path="code" type="String" required={true}>
        The one-time passcode sent to the user's email address.
      </ParamField>

      <ParamField path="sentTo" type="String" required={true}>
        The user's email address.
      </ParamField>

      ### Returns

      <ResponseField name="PrivyUser" type="PrivyUser">
        The updated user object with the newly linked account.
      </ResponseField>

      ### Throws

      An error if linking the account is unsuccessful.

      ## Usage

      ```swift theme={"system"}
      // Send code to user's email
      do {
          try await privy.email.sendCode(to: email)
          // successfully sent code to users email
      } catch {
          print("error sending code to \(email): \(error)")
      }

      // Link the email using the code
      do {
          let user = try await privy.email.linkWithCode(code, sentTo: email)
          print("Email successfully linked!")
          // user has linked their email and user object is updated
      } catch {
          print("error linking email: \(error)")
      }
      ```
    </Tab>

    <Tab title="External Wallets">
      The steps to implementing the `link` flow are analogous to the [login flow's](/authentication/user-authentication/login-methods/wallet#swift) of both Sign in with Ethereum (SIWE) and Sign in with Solana (SIWS).

      <Tabs>
        <Tab title="Ethereum Wallet">
          To link an Ethereum wallet, use the Privy client's `siwe` handler.

          ## Generate SIWE message

          ```swift theme={"system"}
          func generateMessage(params: SiweMessageParams) async throws -> String
          ```

          ### Parameters

          <ParamField path="params" type="SiweMessageParams" required>
            Set of parameters required to generate the message.

            <Expandable defaultOpen="true">
              <ParamField path="appDomain" type="String" required>
                Your app's domain.
              </ParamField>

              <ParamField path="appUri" type="String" required>
                Your app's URI.
              </ParamField>

              <ParamField path="chainId" type="String" required>
                EVM Chain ID, e.g. "1" for Ethereum Mainnet.
              </ParamField>

              <ParamField path="walletAddress" type="String" required>
                The user's [ERC-55](https://eips.ethereum.org/EIPS/eip-55) compliant wallet address.
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="String" type="String">
            The SIWE message that can be signed by the wallet.
          </ResponseField>

          ### Usage

          ```swift theme={"system"}
          let params = SiweMessageParams(
              appDomain: appDomain,
              appUri: appUri,
              chainId: chainId,
              walletAddress: walletAddress
          )

          do {
              let message = try await privy.siwe.generateMessage(params: params)
              // request an EIP-191 `personal_sign` signature on the message
          } catch {
              // An error can be thrown if the network call to generate the message fails,
              // or if invalid metadata was passed in.
          }
          ```

          ## Sign the SIWE message

          Using the message returned by `generateMessage`, request an EIP-191 `personal_sign` signature from the user's connected wallet. You should do this using the library your app uses to connect to external wallets (e.g. the MetaMask SDK or WalletConnect). Once the user successfully signs the message, pass the signature into the `link` function.

          ## Link with SIWE

          ```swift theme={"system"}
          func link(
              message: String,
              signature: String,
              params: SiweMessageParams,
              metadata: WalletLoginMetadata?
          ) async throws -> PrivyUser
          ```

          ### Parameters

          <ParamField path="message" type="String" required>
            The message returned from `generateMessage`.
          </ParamField>

          <ParamField path="signature" type="String" required>
            The signature of the SIWE message, signed by the user's wallet.
          </ParamField>

          <ParamField path="params" type="SiweMessageParams" required>
            The same SiweMessageParams passed into `generateMessage`.
          </ParamField>

          <ParamField path="metadata" type="WalletLoginMetadata?">
            (Optional) Additional metadata that will be stored with the linked wallet.

            <Expandable defaultOpen="true">
              <ParamField path="walletClientType" type="WalletClientType">
                An enum specifying the type of wallet used to link. e.g. `.metamask`
              </ParamField>

              <ParamField path="connectorType" type="String">
                A string identifying how wallet was connected. e.g. "wallet\_connect"
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="PrivyUser" type="PrivyUser">
            The updated user object with the wallet added.
          </ResponseField>

          ### Usage

          ```swift theme={"system"}
          let params = SiweMessageParams(
              appDomain: appDomain,
              appUri: appUri,
              chainId: chainId,
              walletAddress: walletAddress
          )

          // optional metadata
          let metadata = WalletLoginMetadata(
              walletClientType: walletClientType,
              connectorType: connectorType
          )

          do {
              let updatedUser = try await privy.siwe.link(
                  message: message,
                  signature: signature,
                  params: params,
                  metadata: metadata
              )
              // Link success
              // updatedUser contains the updated user object with the wallet added
          } catch {
              // Link failure, either due to invalid signature or network error
          }
          ```
        </Tab>

        <Tab title="Solana Wallet">
          To link a Solana wallet, use the Privy client's `siws` handler.

          ## Generate SIWS message

          ```swift theme={"system"}
          func generateMessage(params: SiwsMessageParams) async throws -> String
          ```

          ### Parameters

          <ParamField path="params" type="SiwsMessageParams" required>
            Set of parameters required to generate the message.

            <Expandable defaultOpen="true">
              <ParamField path="appDomain" type="String" required>
                Your app's domain.
              </ParamField>

              <ParamField path="appUri" type="String" required>
                Your app's URI.
              </ParamField>

              <ParamField path="walletAddress" type="String" required>
                The user's Solana wallet address.
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="String" type="String">
            The SIWS message that can be signed by the wallet.
          </ResponseField>

          ### Usage

          ```swift theme={"system"}
          let params = SiwsMessageParams(
              appDomain: appDomain,
              appUri: appUri,
              walletAddress: walletAddress
          )

          do {
              let message = try await privy.siws.generateMessage(params: params)
              // request a signature on the message from the Solana wallet
          } catch {
              // An error can be thrown if the network call to generate the message fails,
              // or if invalid metadata was passed in.
          }
          ```

          ## Sign the SIWS message

          Using the message returned by `generateMessage`, request a signature from the user's connected Solana wallet. You should do this using the library your app uses to connect to Solana wallets (e.g. the Phantom SDK or Solana Mobile Wallet Adapter). Once the user successfully signs the message, pass the signature into the `link` function.

          ## Link with SIWS

          ```swift theme={"system"}
          func link(
              message: String,
              signature: String,
              metadata: WalletLoginMetadata?
          ) async throws -> PrivyUser
          ```

          ### Parameters

          <ParamField path="message" type="String" required>
            The message returned from `generateMessage`.
          </ParamField>

          <ParamField path="signature" type="String" required>
            The signature of the SIWS message, signed by the user's Solana wallet.
          </ParamField>

          <ParamField path="metadata" type="WalletLoginMetadata?">
            (Optional) Additional metadata that will be stored with the linked wallet.

            <Expandable defaultOpen="true">
              <ParamField path="walletClientType" type="WalletClientType">
                An enum specifying the type of wallet used to link.
              </ParamField>

              <ParamField path="connectorType" type="String">
                A string identifying how wallet was connected.
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="PrivyUser" type="PrivyUser">
            The updated user object with the wallet added.
          </ResponseField>

          ### Usage

          ```swift theme={"system"}
          let params = SiwsMessageParams(
              appDomain: appDomain,
              appUri: appUri,
              walletAddress: walletAddress
          )

          // optional metadata
          let metadata = WalletLoginMetadata(
              walletClientType: walletClientType,
              connectorType: connectorType
          )

          do {
              let updatedUser = try await privy.siws.link(
                  message: message,
                  signature: signature,
                  metadata: metadata
              )
              // Link success
              // updatedUser contains the updated user object with the wallet added
          } catch {
              // Link failure, either due to invalid signature or network error
          }
          ```
        </Tab>
      </Tabs>
    </Tab>

    <Tab title="Passkeys">
      Use the following method from the `passkey` handler to link a new passkey to a user. Be sure to follow iOS passkey setup instructions [here](/authentication/user-authentication/login-methods/passkey#swift).

      ```swift theme={"system"}
      func link(relyingParty: String, displayName: String?) async throws -> PrivyUser
      ```

      ### Returns

      <ResponseField name="PrivyUser" type="PrivyUser">
        The updated user object with the newly linked passkey.
      </ResponseField>

      ## Usage

      ```swift theme={"system"}
      do {
          let displayName = "Optional Display Name"

          let user = try await privy.passkey.link(
              relyingParty: relyingParty,
              displayName: displayName
          )

          // Successfully linked a passkey to an existing user
      } catch {
          print("Signup failed: \(error.localizedDescription)")
      }
      ```
    </Tab>

    <Tab title="OAuth">
      Use the following method from the OAuth handler to link a new OAuth account to an existing user.

      ```swift theme={"system"}
      func link(with provider: OAuthProvider, appUrlScheme: String?) async throws -> PrivyUser
      ```

      ### Returns

      <ResponseField name="PrivyUser" type="PrivyUser">
        The updated user object with the newly linked OAuth account.
      </ResponseField>

      ## Usage

      ```swift theme={"system"}
      do {
          let user = try await privy.oAuth.link(
              with: .google,
              appUrlScheme: "your-app-scheme"  // Optional
          )

          // Successfully linked an OAuth account to an existing user
      } catch {
          print("Linking failed: \(error.localizedDescription)")
      }
      ```
    </Tab>
  </Tabs>
</View>

<View title="Flutter" icon="flutter">
  <Tabs>
    <Tab title="Email/Phone">
      The steps to implementing the `linkWithCode` flow are analogous to the [`loginWithCode` flow](/authentication/user-authentication/login-methods/email#flutter). The same thing applies for phone accounts, using `privy.sms.linkWithCode` similar to how [`privy.sms.loginWithCode`](/authentication/user-authentication/login-methods/sms-whatsapp#flutter) works.

      First, prompt the user for their email address and use the Privy client's `privy.email.sendCode` method to send them a one-time passcode:

      ```dart theme={"system"}
      Future<Result<void>> sendCode(String email)
      ```

      ## Link With Code

      Then, prompt the user for the code they received and use the `linkWithCode` method:

      ```dart theme={"system"}
      Future<Result<PrivyUser>> linkWithCode({
        required String code,
        required String email,
      })
      ```

      <ParamField path="code" type="String" required>
        The one-time passcode sent to the user's email address.
      </ParamField>

      <ParamField path="email" type="String" required>
        The user's email address.
      </ParamField>

      ### Returns

      <ResponseField name="Result<PrivyUser>" type="Result<PrivyUser>">
        A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the newly linked account.
      </ResponseField>

      ## Usage

      ```dart theme={"system"}
      // Send code to user's email
      final sendResult = await privy.email.sendCode("user@test.com");

      sendResult.fold(
        onSuccess: (_) {
          // OTP was successfully sent, now prompt user for code
        },
        onFailure: (error) {
          print("Error sending OTP: ${error.message}");
        },
      );

      // Link the email using the code
      final linkResult = await privy.email.linkWithCode(
        code: "123456",
        email: "user@test.com",
      );

      linkResult.fold(
        onSuccess: (updatedUser) {
          print("Email successfully linked!");
          // updatedUser contains the updated user object with the email added
        },
        onFailure: (error) {
          print("Error linking email: ${error.message}");
        },
      );
      ```
    </Tab>

    <Tab title="External Wallets">
      The steps to implementing the `link` flow are analogous to the [login flows](/authentication/user-authentication/login-methods/wallet#flutter) of both Sign in with Ethereum (SIWE) and Sign in with Solana (SIWS).

      <Tabs>
        <Tab title="Ethereum Wallet">
          To link an Ethereum wallet, use the Privy client's `siwe` handler.

          ## Generate SIWE message

          ```dart theme={"system"}
          Future<Result<String>> generateMessage(SiweMessageParams params)
          ```

          ### Parameters

          <ParamField path="params" type="SiweMessageParams">
            Set of parameters required to generate the message.

            <Expandable defaultOpen="true">
              <ParamField path="appDomain" type="String" required>
                Your app's domain. e.g. "my-domain.com"
              </ParamField>

              <ParamField path="appUri" type="String" required>
                Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)"
              </ParamField>

              <ParamField path="chainId" type="String" required>
                EVM Chain ID, e.g. "1" for Ethereum Mainnet
              </ParamField>

              <ParamField path="walletAddress" type="String" required>
                The user's [ERC-55](https://eips.ethereum.org/EIPS/eip-55) compliant wallet address.
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="Result<String>" type="Result<String>">
            A result type encapsulating the SIWE message that can be signed by the wallet on success.
          </ResponseField>

          ### Usage

          ```dart theme={"system"}
          final params = SiweMessageParams(
            appDomain: domain,
            appUri: uri,
            chainId: chainId,
            walletAddress: walletAddress,
          );

          final result = await privy.siwe.generateMessage(params);

          result.fold(
            onSuccess: (message) {
              // request an EIP-191 `personal_sign` signature on the message
            },
            onFailure: (error) {
              // An error can be thrown if the network call to generate the message fails,
              // or if invalid metadata was passed in.
            },
          );
          ```

          ## Sign the SIWE message

          Using the message returned by `generateMessage`, request an EIP-191 `personal_sign` signature from the user's connected wallet. You should do this using the library your app uses to connect to external wallets (e.g. the MetaMask SDK or WalletConnect). Once the user successfully signs the message, pass the signature into the `link` function.

          ## Link with SIWE

          ```dart theme={"system"}
          Future<Result<PrivyUser>> link({
            required String message,
            required String signature,
            required SiweMessageParams params,
            WalletLoginMetadata? metadata,
          })
          ```

          ### Parameters

          <ParamField path="message" type="String" required>
            The message returned from "generateMessage".
          </ParamField>

          <ParamField path="signature" type="String" required>
            The signature of the SIWE message, signed by the user's wallet.
          </ParamField>

          <ParamField path="params" type="SiweMessageParams" required>
            The same SiweMessageParams passed into "generateMessage".
          </ParamField>

          <ParamField path="metadata" type="WalletLoginMetadata">
            (Optional) you can pass additional metadata that will be stored with the linked wallet.

            <Expandable defaultOpen="true">
              <ParamField path="walletClientType" type="WalletClientType">
                An enum specifying the type of wallet used to link. e.g. WalletClientType.metamask
              </ParamField>

              <ParamField path="connectorType" type="String">
                A string identifying how wallet was connected. e.g. "wallet\_connect"
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="Result<PrivyUser>" type="Result<PrivyUser>">
            A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the newly linked wallet.
          </ResponseField>

          ### Usage

          ```dart theme={"system"}
          final params = SiweMessageParams(
            appDomain: domain,
            appUri: uri,
            chainId: chainId,
            walletAddress: walletAddress,
          );

          // optional metadata
          final metadata = WalletLoginMetadata(
            walletClientType: walletClient,
            connectorType: connectorType,
          );

          final result = await privy.siwe.link(
            message: message,
            signature: signature,
            params: params,
            metadata: metadata,
          );

          result.fold(
            onSuccess: (updatedUser) {
              // Link success
              // updatedUser contains the updated user object with the wallet added
            },
            onFailure: (error) {
              // Link failure, either due to invalid signature or network error
            },
          );
          ```
        </Tab>

        <Tab title="Solana Wallet">
          To link a Solana wallet, use the Privy client's `siws` handler.

          ## Generate SIWS message

          ```dart theme={"system"}
          Future<Result<String>> generateMessage(SiwsMessageParams params)
          ```

          ### Parameters

          <ParamField path="params" type="SiwsMessageParams">
            Set of parameters required to generate the message.

            <Expandable defaultOpen="true">
              <ParamField path="appDomain" type="String" required>
                Your app's domain. e.g. "my-domain.com"
              </ParamField>

              <ParamField path="appUri" type="String" required>
                Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)"
              </ParamField>

              <ParamField path="walletAddress" type="String" required>
                The user's Solana wallet address.
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="Result<String>" type="Result<String>">
            A result type encapsulating the SIWS message that can be signed by the wallet on success.
          </ResponseField>

          ### Usage

          ```dart theme={"system"}
          final params = SiwsMessageParams(
            appDomain: domain,
            appUri: uri,
            walletAddress: walletAddress,
          );

          final result = await privy.siws.generateMessage(params);

          result.fold(
            onSuccess: (message) {
              // request a signature on the message from the Solana wallet
            },
            onFailure: (error) {
              // An error can be thrown if the network call to generate the message fails,
              // or if invalid metadata was passed in.
            },
          );
          ```

          ## Sign the SIWS message

          Using the message returned by `generateMessage`, request a signature from the user's connected Solana wallet. You should do this using the library your app uses to connect to Solana wallets (e.g. the Phantom SDK or Solana Mobile Wallet Adapter). Once the user successfully signs the message, pass the signature into the `link` function.

          ## Link with SIWS

          ```dart theme={"system"}
          Future<Result<PrivyUser>> link({
            required String message,
            required String signature,
            required SiwsMessageParams params,
            WalletLoginMetadata? metadata,
          })
          ```

          ### Parameters

          <ParamField path="message" type="String" required>
            The message returned from "generateMessage".
          </ParamField>

          <ParamField path="signature" type="String" required>
            The signature of the SIWS message, signed by the user's Solana wallet.
          </ParamField>

          <ParamField path="params" type="SiwsMessageParams" required>
            The same SiwsMessageParams passed into "generateMessage".
          </ParamField>

          <ParamField path="metadata" type="WalletLoginMetadata">
            (Optional) you can pass additional metadata that will be stored with the linked wallet.

            <Expandable defaultOpen="true">
              <ParamField path="walletClientType" type="WalletClientType">
                An enum specifying the type of wallet used to link. e.g. WalletClientType.phantom
              </ParamField>

              <ParamField path="connectorType" type="String">
                A string identifying how wallet was connected. e.g. "wallet\_connect"
              </ParamField>
            </Expandable>
          </ParamField>

          ### Returns

          <ResponseField name="Result<PrivyUser>" type="Result<PrivyUser>">
            A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the newly linked wallet.
          </ResponseField>

          ### Usage

          ```dart theme={"system"}
          final params = SiwsMessageParams(
            appDomain: domain,
            appUri: uri,
            walletAddress: walletAddress,
          );

          // optional metadata
          final metadata = WalletLoginMetadata(
            walletClientType: walletClient,
            connectorType: connectorType,
          );

          final result = await privy.siws.link(
            message: message,
            signature: signature,
            params: params,
            metadata: metadata,
          );

          result.fold(
            onSuccess: (updatedUser) {
              // Link success
              // updatedUser contains the updated user object with the wallet added
            },
            onFailure: (error) {
              // Link failure, either due to invalid signature or network error
            },
          );
          ```
        </Tab>
      </Tabs>
    </Tab>

    <Tab title="Passkeys">
      Use the following method from the `passkey` handler to link a new passkey to a user. Be sure to follow the [Android](/basics/android/advanced/setup-passkeys) and [iOS](/authentication/user-authentication/login-methods/passkey#swift) setup instructions.

      ```dart theme={"system"}
      Future<Result<PrivyUser>> link({
        required String relyingParty,
        String? displayName,
      })
      ```

      ### Parameters

      <ParamField path="relyingParty" type="String" required>
        The URL origin where your Digital Asset Links are available (e.g., `https://example.com`).
      </ParamField>

      <ParamField path="displayName" type="String">
        An optional display name to associate with the passkey. This name will be shown to the user when selecting which passkey to use for authentication.
      </ParamField>

      ### Returns

      <ResponseField name="Result<PrivyUser>" type="Result<PrivyUser>">
        A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the newly linked passkey.
      </ResponseField>

      ## Usage

      ```dart theme={"system"}
      final relyingParty = "https://yourdomain.com";
      final displayName = "Optional Display Name"; // Optional parameter

      final result = await privy.passkey.link(
        relyingParty: relyingParty,
        displayName: displayName,
      );

      result.fold(
        onSuccess: (updatedUser) {
          // Successfully linked a passkey to an existing user
          // updatedUser contains the updated user object with the passkey added
          print("Passkey linked successfully!");
        },
        onFailure: (error) {
          // Handle error
          print("Link failed: ${error.message}");
        },
      );
      ```
    </Tab>

    <Tab title="OAuth">
      Use the following method from the OAuth handler to link a new OAuth account to an existing user:

      ```dart theme={"system"}
      Future<Result<PrivyUser>> link({
        required OAuthProvider provider,
        required String appUrlScheme,
      })
      ```

      ### Parameters

      <ParamField path="provider" type="OAuthProvider" required>
        The OAuth provider to link (e.g., `OAuthProvider.google`, `OAuthProvider.discord`, `OAuthProvider.twitter`, `OAuthProvider.apple`, `OAuthProvider.telegram`).
      </ParamField>

      <ParamField path="appUrlScheme" type="String" required>
        Custom URL scheme for redirecting back to the app after OAuth authentication.
      </ParamField>

      ### Returns

      <ResponseField name="Result<PrivyUser>" type="Result<PrivyUser>">
        A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the newly linked OAuth account.
      </ResponseField>

      ## Usage

      ```dart theme={"system"}
      final result = await privy.oAuth.link(
        provider: OAuthProvider.google,
        appUrlScheme: "your-app-scheme",
      );

      result.fold(
        onSuccess: (user) {
          // Successfully linked an OAuth account to an existing user
        },
        onFailure: (error) {
          print("Linking failed: ${error.message}");
        },
      );
      ```
    </Tab>
  </Tabs>
</View>
