From 535d0e709dc3cb4a9d8310c8834943a3edfe546f Mon Sep 17 00:00:00 2001 From: akshatextreme Date: Sun, 22 Feb 2026 02:17:59 +0400 Subject: [PATCH 1/2] [SDK] Add name and picture fields to Profile type for Apple Sign In compliance - Add optional name and picture fields to Profile type to support OAuth provider data - Update LinkedProfilesScreen to display user's name when available - Fixes #8673: Apple App Store rejection due to missing display name The Profile type now includes name and picture fields that OAuth providers like Google and Apple can populate. The UI prioritizes displaying the user's full name when available, improving Apple Sign In compliance with App Store Guideline 4.8. --- .changeset/apple-profile-name-picture.md | 5 +++++ .../web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx | 5 +++++ .../thirdweb/src/wallets/in-app/core/authentication/types.ts | 2 ++ 3 files changed, 12 insertions(+) create mode 100644 .changeset/apple-profile-name-picture.md 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.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx index b2b1eaf0e3d..11343376db3 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx @@ -24,6 +24,11 @@ import { MenuButton } from "../MenuButton.js"; import type { WalletDetailsModalScreen } from "./types.js"; function getProfileDisplayName(profile: Profile) { + // Prefer name if available (from OAuth providers like Google/Apple) + if (profile.details.name) { + return profile.details.name; + } + switch (true) { case profile.type === "email" && profile.details.email !== undefined: return profile.details.email; 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..ef3bb3366a7 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,8 @@ export type Profile = { email?: string; phone?: string; address?: Address; + name?: string; + picture?: string; }; }; From a7e340b5f9aa50e7a426b9e739a5a5c02478bc54 Mon Sep 17 00:00:00 2001 From: akshatextreme Date: Thu, 5 Mar 2026 02:29:58 +0400 Subject: [PATCH 2/2] Address PR review feedback: trim whitespace, integrate picture field, add JSDoc, add tests --- .../screens/LinkedProfilesScreen.test.tsx | 78 +++++++++++++++++++ .../screens/LinkedProfilesScreen.tsx | 41 +++++++--- .../in-app/core/authentication/types.ts | 2 + 3 files changed, 110 insertions(+), 11 deletions(-) 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 11343376db3..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,38 +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) { - // Prefer name if available (from OAuth providers like Google/Apple) - if (profile.details.name) { - return profile.details.name; +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: @@ -159,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 ef3bb3366a7..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,7 +124,9 @@ 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; }; };