diff --git a/.changeset/apple-profile-name-picture.md b/.changeset/apple-profile-name-picture.md
new file mode 100644
index 00000000000..b34d4708507
--- /dev/null
+++ b/.changeset/apple-profile-name-picture.md
@@ -0,0 +1,5 @@
+---
+"thirdweb": minor
+---
+
+Add optional `name` and `picture` fields to `Profile` type to support additional OAuth provider data (Google, Apple, etc.). The UI now displays the user's name when available from OAuth providers, improving Apple Sign In compliance.
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.test.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.test.tsx
index d62053b8fe2..d46020e8fc9 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.test.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.test.tsx
@@ -110,6 +110,84 @@ describe("LinkedProfilesScreen", () => {
expect(screen.getByText("Unknown")).toBeInTheDocument();
});
+ it("should display name from profile.details.name when available", () => {
+ vi.mocked(useProfiles).mockReturnValue({
+ data: [
+ {
+ details: { email: "apple@example.com", name: "Jane Doe" },
+ type: "apple",
+ },
+ ],
+ isLoading: false,
+ // biome-ignore lint/suspicious/noExplicitAny: Mocking data
+ } as any);
+
+ render();
+ expect(screen.getByText("Jane Doe")).toBeInTheDocument();
+ expect(screen.queryByText("apple@example.com")).not.toBeInTheDocument();
+ });
+
+ it("should fall back to email when profile.details.name is an empty string", () => {
+ vi.mocked(useProfiles).mockReturnValue({
+ data: [
+ { details: { email: "apple@example.com", name: "" }, type: "google" },
+ ],
+ isLoading: false,
+ // biome-ignore lint/suspicious/noExplicitAny: Mocking data
+ } as any);
+
+ render();
+ expect(screen.getByText("apple@example.com")).toBeInTheDocument();
+ });
+
+ it("should fall back to email when profile.details.name is whitespace only", () => {
+ vi.mocked(useProfiles).mockReturnValue({
+ data: [
+ {
+ details: { email: "google@example.com", name: " " },
+ type: "google",
+ },
+ ],
+ isLoading: false,
+ // biome-ignore lint/suspicious/noExplicitAny: Mocking data
+ } as any);
+
+ render();
+ expect(screen.getByText("google@example.com")).toBeInTheDocument();
+ });
+
+ it("should fall back to email when profile.details.name is undefined", () => {
+ vi.mocked(useProfiles).mockReturnValue({
+ data: [
+ {
+ details: { email: "google@example.com", name: undefined },
+ type: "google",
+ },
+ ],
+ isLoading: false,
+ // biome-ignore lint/suspicious/noExplicitAny: Mocking data
+ } as any);
+
+ render();
+ expect(screen.getByText("google@example.com")).toBeInTheDocument();
+ });
+
+ it("should display trimmed name when profile.details.name has leading/trailing spaces", () => {
+ vi.mocked(useProfiles).mockReturnValue({
+ data: [
+ {
+ details: { email: "apple@example.com", name: " John Doe " },
+ type: "apple",
+ },
+ ],
+ isLoading: false,
+ // biome-ignore lint/suspicious/noExplicitAny: Mocking data
+ } as any);
+
+ render();
+ expect(screen.getByText("John Doe")).toBeInTheDocument();
+ });
+
it("should not display guest profiles", () => {
vi.mocked(useProfiles).mockReturnValue({
data: [{ details: {}, type: "guest" }],
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx
index b2b1eaf0e3d..83b1190ac82 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx
@@ -9,33 +9,39 @@ import { useSocialProfiles } from "../../../../core/social/useSocialProfiles.js"
import { getSocialIcon } from "../../../../core/utils/walletIcon.js";
import { useProfiles } from "../../../hooks/wallets/useProfiles.js";
import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js";
-import { Container, Line, ModalHeader } from "../../components/basic.js";
-import { IconButton } from "../../components/buttons.js";
import { Img } from "../../components/Img.js";
import { Spacer } from "../../components/Spacer.js";
+import { Container, Line, ModalHeader } from "../../components/basic.js";
+import { IconButton } from "../../components/buttons.js";
import { Text } from "../../components/text.js";
import { Blobbie } from "../Blobbie.js";
+import { MenuButton } from "../MenuButton.js";
import { AddUserIcon } from "../icons/AddUserIcon.js";
import { EmailIcon } from "../icons/EmailIcon.js";
import { FingerPrintIcon } from "../icons/FingerPrintIcon.js";
import { PhoneIcon } from "../icons/PhoneIcon.js";
import type { ConnectLocale } from "../locale/types.js";
-import { MenuButton } from "../MenuButton.js";
import type { WalletDetailsModalScreen } from "./types.js";
-function getProfileDisplayName(profile: Profile) {
+function getProfileDisplayName(profile: Profile): string {
+ // Prefer name if available (from OAuth providers like Google/Apple).
+ // Use .trim() to reject whitespace-only strings that some backends may return.
+ if (profile.details.name?.trim()) {
+ return profile.details.name.trim();
+ }
+
switch (true) {
case profile.type === "email" && profile.details.email !== undefined:
- return profile.details.email;
+ return profile.details.email as string;
case profile.type === "google" && profile.details.email !== undefined:
- return profile.details.email;
+ return profile.details.email as string;
case profile.type === "phone" && profile.details.phone !== undefined:
- return profile.details.phone;
+ return profile.details.phone as string;
case profile.details.address !== undefined:
return shortenAddress(profile.details.address, 6);
case (profile.type as string) === "cognito" &&
profile.details.email !== undefined:
- return profile.details.email;
+ return profile.details.email as string;
case (profile.type as string).toLowerCase() === "custom_auth_endpoint":
return "Custom Profile";
default:
@@ -154,6 +160,18 @@ function LinkedProfile({
}}
width={iconSize.lg}
/>
+ ) : profile.details.picture ? (
+ // Fallback to OAuth provider picture (e.g. Google/Apple) when no social profile avatar is available
+
) : profile.details.address !== undefined ? (
+ {/*
+ * Name display priority:
+ * 1. socialProfiles name (real-time on-chain/social lookup)
+ * 2. profile.details.name (OAuth provider name, e.g. Google/Apple)
+ * 3. email / phone / shortened address / profile type (via getProfileDisplayName)
+ */}
{socialProfiles?.find((p) => p.avatar)?.name ||
getProfileDisplayName(profile)}
diff --git a/packages/thirdweb/src/wallets/in-app/core/authentication/types.ts b/packages/thirdweb/src/wallets/in-app/core/authentication/types.ts
index f91eb56040e..58e35ff5654 100644
--- a/packages/thirdweb/src/wallets/in-app/core/authentication/types.ts
+++ b/packages/thirdweb/src/wallets/in-app/core/authentication/types.ts
@@ -124,6 +124,10 @@ export type Profile = {
email?: string;
phone?: string;
address?: Address;
+ /** Full display name from OAuth provider (e.g. Google, Apple). Populated on first sign-in. */
+ name?: string;
+ /** Profile picture URL from OAuth provider (e.g. Google, Apple). Populated on first sign-in. */
+ picture?: string;
};
};