From 50200763677b6ba45905b3d1126ba2d82e3c2bc7 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Wed, 22 Oct 2025 05:49:11 +0700 Subject: [PATCH 1/2] Add userId support across wallet APIs and UI Introduces support for the userId field throughout wallet-related APIs and dashboard components. Updates search, display, and transformation logic to handle userId in addition to existing identifiers, and improves UI to distinguish between 'User Identifier' (userId) and 'Auth Identifier'. Updates API client types and documentation to reflect the new userId field. Closes BLD-447 --- .changeset/breezy-heads-crash.md | 5 ++ .../AdvancedSearchInput.tsx | 14 +++++- .../SearchResults.tsx | 41 +++++++++++++++- .../in-app-wallet-users-content.tsx | 47 ++++++++++++++----- .../searchUsers.ts | 7 ++- .../in-app-wallet-users-content/types.ts | 3 +- .../src/@/hooks/useEmbeddedWallets.ts | 4 +- packages/api/src/client/sdk.gen.ts | 2 + packages/api/src/client/types.gen.ts | 28 +++++++++++ 9 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 .changeset/breezy-heads-crash.md diff --git a/.changeset/breezy-heads-crash.md b/.changeset/breezy-heads-crash.md new file mode 100644 index 00000000000..d661321e877 --- /dev/null +++ b/.changeset/breezy-heads-crash.md @@ -0,0 +1,5 @@ +--- +"@thirdweb-dev/api": patch +--- + +added support for userId across wallet apis diff --git a/apps/dashboard/src/@/components/in-app-wallet-users-content/AdvancedSearchInput.tsx b/apps/dashboard/src/@/components/in-app-wallet-users-content/AdvancedSearchInput.tsx index 2e6d0eddf3f..b037d66e1ac 100644 --- a/apps/dashboard/src/@/components/in-app-wallet-users-content/AdvancedSearchInput.tsx +++ b/apps/dashboard/src/@/components/in-app-wallet-users-content/AdvancedSearchInput.tsx @@ -14,6 +14,15 @@ import { } from "@/components/ui/select"; import type { SearchType } from "./types"; +const searchTypeLabels: Record = { + email: "Email", + phone: "Phone", + id: "Auth Identifier", + address: "Address", + externalWallet: "External Wallet", + userId: "User Identifier", +}; + export function AdvancedSearchInput(props: { onSearch: (searchType: SearchType, query: string) => void; onClear: () => void; @@ -52,7 +61,8 @@ export function AdvancedSearchInput(props: { Email Phone - ID + Auth Identifier + User Identifier Address External Wallet @@ -62,7 +72,7 @@ export function AdvancedSearchInput(props: {
setQuery(e.target.value)} onKeyDown={handleKeyDown} diff --git a/apps/dashboard/src/@/components/in-app-wallet-users-content/SearchResults.tsx b/apps/dashboard/src/@/components/in-app-wallet-users-content/SearchResults.tsx index 701d0e71161..c6dae0487a4 100644 --- a/apps/dashboard/src/@/components/in-app-wallet-users-content/SearchResults.tsx +++ b/apps/dashboard/src/@/components/in-app-wallet-users-content/SearchResults.tsx @@ -12,8 +12,9 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { CopyTextButton } from "../ui/CopyTextButton"; -const getUserIdentifier = (user: WalletUser) => { +const getAuthIdentifier = (user: WalletUser) => { const mainDetail = user.linkedAccounts[0]?.details; return ( mainDetail?.email ?? @@ -24,6 +25,13 @@ const getUserIdentifier = (user: WalletUser) => { ); }; +const truncateIdentifier = (value: string) => { + if (value.length <= 18) { + return value; + } + return `${value.slice(0, 8)}...${value.slice(-4)}`; +}; + export function SearchResults(props: { results: WalletUser[]; client: ThirdwebClient; @@ -51,6 +59,8 @@ export function SearchResults(props: { const mainDetail = user.linkedAccounts?.[0]?.details; const email = mainDetail?.email as string | undefined; const phone = mainDetail?.phone as string | undefined; + const authIdentifier = getAuthIdentifier(user); + const userIdentifier = user.id; // Get external wallet addresses from linkedAccounts where type is 'siwe' const externalWalletAccounts = @@ -68,7 +78,34 @@ export function SearchResults(props: {

User Identifier

-

{getUserIdentifier(user)}

+ {userIdentifier ? ( + + ) : ( +

N/A

+ )} +
+ +
+

+ Auth Identifier +

+ {authIdentifier ? ( + + ) : ( +

N/A

+ )}
{walletAddress && ( diff --git a/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx b/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx index 95fe1e3501a..b58f1d836ea 100644 --- a/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx +++ b/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx @@ -28,7 +28,7 @@ import { SearchResults } from "./SearchResults"; import { searchUsers } from "./searchUsers"; import type { SearchType } from "./types"; -const getUserIdentifier = (accounts: WalletUser["linkedAccounts"]) => { +const getAuthIdentifier = (accounts: WalletUser["linkedAccounts"]) => { const mainDetail = accounts[0]?.details; return ( mainDetail?.email ?? @@ -44,6 +44,13 @@ const getExternalWallets = (accounts: WalletUser["linkedAccounts"]) => { const columnHelper = createColumnHelper(); +const truncateIdentifier = (value: string) => { + if (value.length <= 18) { + return value; + } + return `${value.slice(0, 8)}...${value.slice(-4)}`; +}; + export function InAppWalletUsersPageContent( props: { authToken: string; @@ -56,9 +63,30 @@ export function InAppWalletUsersPageContent( ) { const columns = useMemo(() => { return [ + columnHelper.accessor("id", { + cell: (cell) => { + const userId = cell.getValue(); + + if (!userId) { + return "N/A"; + } + + return ( + + ); + }, + header: "User Identifier", + id: "user_identifier", + }), columnHelper.accessor("linkedAccounts", { cell: (cell) => { - const identifier = getUserIdentifier(cell.getValue()); + const identifier = getAuthIdentifier(cell.getValue()); if (!identifier) { return "N/A"; @@ -66,21 +94,17 @@ export function InAppWalletUsersPageContent( return ( 30 - ? `${identifier.slice(0, 30)}...` - : identifier - } + textToShow={truncateIdentifier(identifier)} textToCopy={identifier} - tooltip="Copy User Identifier" + tooltip="Copy Auth Identifier" copyIconPosition="left" variant="ghost" /> ); }, enableColumnFilter: true, - header: "User Identifier", - id: "user_identifier", + header: "Auth Identifier", + id: "auth_identifier", }), columnHelper.accessor("wallets", { cell: (cell) => { @@ -243,7 +267,8 @@ export function InAppWalletUsersPageContent( : "Wallet not created yet", external_wallets: externalWalletAddresses || "None", login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "), - user_identifier: getUserIdentifier(row.linkedAccounts), + auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A", + user_identifier: row.id || "N/A", }; }), ); diff --git a/apps/dashboard/src/@/components/in-app-wallet-users-content/searchUsers.ts b/apps/dashboard/src/@/components/in-app-wallet-users-content/searchUsers.ts index 36fe6882b5e..918016a200a 100644 --- a/apps/dashboard/src/@/components/in-app-wallet-users-content/searchUsers.ts +++ b/apps/dashboard/src/@/components/in-app-wallet-users-content/searchUsers.ts @@ -18,10 +18,10 @@ configure({ type APIWallet = ListUserWalletsResponses[200]["result"]["wallets"][0]; type APIProfile = APIWallet["profiles"][0]; -// Transform API response to match existing WalletUser format +// Transform API response to match wallet user format function transformToWalletUser(apiWallet: APIWallet): WalletUser { return { - id: getProfileId(apiWallet.profiles[0]) || "", + id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "", linkedAccounts: apiWallet.profiles.map((profile) => { // Create details object based on the profile data let details: @@ -105,6 +105,9 @@ export async function searchUsers( case "externalWallet": queryParams.externalWalletAddress = query; break; + case "userId": + queryParams.userId = query; + break; } // Use the generated API function with Bearer authentication diff --git a/apps/dashboard/src/@/components/in-app-wallet-users-content/types.ts b/apps/dashboard/src/@/components/in-app-wallet-users-content/types.ts index fa825d6a8d1..d30927c0880 100644 --- a/apps/dashboard/src/@/components/in-app-wallet-users-content/types.ts +++ b/apps/dashboard/src/@/components/in-app-wallet-users-content/types.ts @@ -3,4 +3,5 @@ export type SearchType = | "phone" | "id" | "address" - | "externalWallet"; + | "externalWallet" + | "userId"; diff --git a/apps/dashboard/src/@/hooks/useEmbeddedWallets.ts b/apps/dashboard/src/@/hooks/useEmbeddedWallets.ts index 1ca70f938a4..20ab3a0e34a 100644 --- a/apps/dashboard/src/@/hooks/useEmbeddedWallets.ts +++ b/apps/dashboard/src/@/hooks/useEmbeddedWallets.ts @@ -76,10 +76,10 @@ const fetchAccountList = ({ }; }; -// Transform API response to match existing WalletUser format +// Transform API response to match the wallet user format function transformToWalletUser(apiWallet: APIWallet): WalletUser { return { - id: getProfileId(apiWallet.profiles[0]) || "", + id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "", linkedAccounts: apiWallet.profiles.map((profile) => { // Create details object based on the profile data let details: diff --git a/packages/api/src/client/sdk.gen.ts b/packages/api/src/client/sdk.gen.ts index 013925ca486..56bebbd45a9 100644 --- a/packages/api/src/client/sdk.gen.ts +++ b/packages/api/src/client/sdk.gen.ts @@ -244,6 +244,7 @@ export const initiateAuthentication = ( * - `isNewUser` - Whether this is a new wallet creation * - `token` - JWT token for authenticated API requests * - `type` - The authentication method used + * - `userId` - Unique identifier for the authenticated user * - `walletAddress` - Your new or existing wallet address * * **Next step - Verify your token:** @@ -442,6 +443,7 @@ export const socialAuthentication = ( * Retrieve the authenticated user's wallet information including wallet addresses and linked authentication wallets. This endpoint provides comprehensive user data for the currently authenticated session. * * **Returns:** + * - userId - Unique identifier for this wallet in thirdweb auth * - Primary wallet address * - Smart wallet address (if available) * - Wallet creation timestamp diff --git a/packages/api/src/client/types.gen.ts b/packages/api/src/client/types.gen.ts index 2a68b900335..7c7f6ba61b3 100644 --- a/packages/api/src/client/types.gen.ts +++ b/packages/api/src/client/types.gen.ts @@ -330,6 +330,10 @@ export type CompleteAuthenticationResponses = { * Type of authentication completed */ type: string; + /** + * Unique identifier for the authenticated user + */ + userId: string; /** * The wallet address */ @@ -875,6 +879,10 @@ export type GetMyWalletResponses = { */ 200: { result: { + /** + * Unique identifier for the user wallet within the thirdweb auth system. + */ + userId?: string; /** * The EOA (Externally Owned Wallet) address of the wallet. This is the traditional wallet address. */ @@ -1076,6 +1084,10 @@ export type ListUserWalletsData = { address?: string; externalWalletAddress?: string; id?: string; + /** + * Filter results by the unique user identifier returned by auth flows. + */ + userId?: string; }; url: "/v1/wallets/user"; }; @@ -1122,6 +1134,10 @@ export type ListUserWalletsResponses = { * Array of user wallets */ wallets: Array<{ + /** + * Unique identifier for the user wallet within the thirdweb auth system. + */ + userId?: string; /** * The EOA (Externally Owned Wallet) address of the wallet. This is the traditional wallet address. */ @@ -1362,6 +1378,10 @@ export type CreateUserWalletResponses = { */ 200: { result: { + /** + * Unique identifier for the user wallet within the thirdweb auth system. + */ + userId?: string; /** * The EOA (Externally Owned Wallet) address of the wallet. This is the traditional wallet address. */ @@ -1604,6 +1624,10 @@ export type ListServerWalletsResponses = { * Array of server wallets */ wallets: Array<{ + /** + * Unique identifier for the user wallet within the thirdweb auth system. + */ + userId?: string; /** * The EOA (Externally Owned Wallet) address of the wallet. This is the traditional wallet address. */ @@ -1831,6 +1855,10 @@ export type CreateServerWalletResponses = { */ 200: { result: { + /** + * Unique identifier for the user wallet within the thirdweb auth system. + */ + userId?: string; /** * The EOA (Externally Owned Wallet) address of the wallet. This is the traditional wallet address. */ From 26f951c524205ff313dda2f40f9c03bc535cc4e2 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Wed, 22 Oct 2025 06:19:45 +0700 Subject: [PATCH 2/2] Refactor user email display in wallet users table Replaces the 'External Wallets' column with an 'Email' column in the in-app wallet users table. Adds a utility to extract the primary email from linked accounts and updates CSV export to include the email instead of external wallet addresses. Simplifies the getAuthIdentifier logic to prioritize the 'id' field. --- .../SearchResults.tsx | 5 +- .../in-app-wallet-users-content.tsx | 66 +++++++++---------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/apps/dashboard/src/@/components/in-app-wallet-users-content/SearchResults.tsx b/apps/dashboard/src/@/components/in-app-wallet-users-content/SearchResults.tsx index c6dae0487a4..ec1c664a197 100644 --- a/apps/dashboard/src/@/components/in-app-wallet-users-content/SearchResults.tsx +++ b/apps/dashboard/src/@/components/in-app-wallet-users-content/SearchResults.tsx @@ -17,11 +17,10 @@ import { CopyTextButton } from "../ui/CopyTextButton"; const getAuthIdentifier = (user: WalletUser) => { const mainDetail = user.linkedAccounts[0]?.details; return ( + mainDetail?.id ?? mainDetail?.email ?? mainDetail?.phone ?? - mainDetail?.address ?? - mainDetail?.id ?? - user.id + mainDetail?.address ); }; diff --git a/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx b/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx index b58f1d836ea..280c8aa14e6 100644 --- a/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx +++ b/apps/dashboard/src/@/components/in-app-wallet-users-content/in-app-wallet-users-content.tsx @@ -31,15 +31,28 @@ import type { SearchType } from "./types"; const getAuthIdentifier = (accounts: WalletUser["linkedAccounts"]) => { const mainDetail = accounts[0]?.details; return ( + mainDetail?.id ?? mainDetail?.email ?? mainDetail?.phone ?? - mainDetail?.address ?? - mainDetail?.id + mainDetail?.address ); }; -const getExternalWallets = (accounts: WalletUser["linkedAccounts"]) => { - return accounts?.filter((account) => account.type === "siwe") || []; +const getPrimaryEmail = (accounts: WalletUser["linkedAccounts"]) => { + const emailFromPrimary = accounts[0]?.details?.email; + if (emailFromPrimary) { + return emailFromPrimary; + } + + const emailAccount = accounts.find((account) => { + return typeof account.details?.email === "string" && account.details.email; + }); + + if (emailAccount && typeof emailAccount.details.email === "string") { + return emailAccount.details.email; + } + + return undefined; }; const columnHelper = createColumnHelper(); @@ -118,33 +131,24 @@ export function InAppWalletUsersPageContent( }), columnHelper.accessor("linkedAccounts", { cell: (cell) => { - const externalWallets = getExternalWallets(cell.getValue()); - if (externalWallets.length === 0) { - return None; + const email = getPrimaryEmail(cell.getValue()); + + if (!email) { + return N/A; } + return ( -
- {externalWallets.slice(0, 2).map((account) => { - const address = account.details?.address as string | undefined; - return address ? ( -
- -
- ) : null; - })} - {externalWallets.length > 2 && ( - - +{externalWallets.length - 2} more - - )} -
+ ); }, - header: "External Wallets", - id: "external_wallets", + header: "Email", + id: "email", }), columnHelper.accessor("wallets", { cell: (cell) => { @@ -254,18 +258,14 @@ export function InAppWalletUsersPageContent( }); const csv = Papa.unparse( usersWallets.map((row) => { - const externalWallets = getExternalWallets(row.linkedAccounts); - const externalWalletAddresses = externalWallets - .map((account) => account.details?.address) - .filter(Boolean) - .join(", "); + const email = getPrimaryEmail(row.linkedAccounts); return { address: row.wallets[0]?.address || "Uninitialized", created: row.wallets[0]?.createdAt ? new Date(row.wallets[0].createdAt).toISOString() : "Wallet not created yet", - external_wallets: externalWalletAddresses || "None", + email: email || "N/A", login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "), auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A", user_identifier: row.id || "N/A",