[PRO-144] SDK: Update in-app wallet icon in wide connect ui (#8556)

<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the in-app wallet feature by updating icons, adding new components for different authentication methods, and improving the UI for wallet connection options.

### Detailed summary
- Updated the in-app wallet icon in the wide connect UI.
- Changed `defaultAuthOptions` from a constant to an exported variable.
- Modified `EmailIcon` component for better styling and structure.
- Added new functions for various modal configurations (`ConfiguredInAppWalletWideModal`, `GoogleLoginWideModal`, `GithubLoginWideModal`).
- Introduced `InAppWalletIcon` component to handle multiple authentication methods visually.
- Created comprehensive storybook examples for `InAppWalletIcon` with various authentication options.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Added an in-app wallet icon that displays available authentication methods and adapts layout for single vs. multiple options.

* **UI/UX Improvements**
  * Updated wallet icon visuals and dynamic color handling for more consistent rendering in the connect modal.
  * Simplified email icon visuals for clearer strokes.

* **Stories**
  * Added Storybook examples showcasing the in-app wallet icon across themes and auth-option permutations.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
MananTank
2025-12-12 21:03:26 +00:00
parent 759f41b11e
commit 151127d668
8 changed files with 456 additions and 23 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---
Update in-app wallet icon in wide connect ui
@@ -17,6 +17,7 @@ import { Skeleton } from "../components/Skeleton.js";
import { Text } from "../components/text.js";
import { WalletImage } from "../components/WalletImage.js";
import { StyledButton } from "../design-system/elements.js";
import { InAppWalletIcon } from "./in-app-wallet-icon.js";
import type { ConnectLocale } from "./locale/types.js";
/**
@@ -52,9 +53,20 @@ export function WalletEntryButton(props: {
wallet && walletId === "inApp"
? (wallet.getConfig() as InAppWalletCreationOptions)?.metadata
: undefined;
const nameOverride = customMeta?.name || walletName;
let nameOverride = customMeta?.name || walletName;
const iconOverride = customMeta?.icon;
// change "Social Login" to name of the login method if only 1 method is enabled
if (wallet.id === "inApp") {
const config = wallet.getConfig() as InAppWalletCreationOptions;
if (config?.auth?.options.length === 1) {
const name = config.auth?.options[0];
if (name) {
nameOverride = uppercaseFirstLetter({ text: name });
}
}
}
return (
<WalletButtonEl
className={props.className}
@@ -64,12 +76,17 @@ export function WalletEntryButton(props: {
>
{iconOverride ? (
<Img
alt={nameOverride}
alt=""
client={props.client}
height={`${iconSize.xl}`}
src={iconOverride}
width={`${iconSize.xl}`}
/>
) : wallet.id === "inApp" ? (
<InAppWalletIcon
client={props.client}
wallet={wallet as Wallet<"inApp">}
/>
) : (
<WalletImage client={props.client} id={walletId} size={iconSize.xl} />
)}
@@ -119,3 +136,7 @@ export const WalletButtonEl = /* @__PURE__ */ StyledButton((_) => {
width: "100%",
};
});
function uppercaseFirstLetter(props: { text: string }) {
return props.text.charAt(0).toUpperCase() + props.text.slice(1);
}
@@ -1,30 +1,23 @@
import type { IconFC } from "./types.js";
/**
* @internal
*/
export const EmailIcon: IconFC = (props) => {
return (
<svg
fill="none"
height={props.size}
role="presentation"
viewBox="0 0 16 16"
width={props.size}
xmlns="http://www.w3.org/2000/svg"
width={props.size}
height={props.size}
viewBox="0 0 24 24"
role="presentation"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="lucide lucide-mail-icon lucide-mail"
style={{ color: props.color }}
>
<path
d="M13.3335 2.6665H2.66683C1.93045 2.6665 1.3335 3.26346 1.3335 3.99984V11.9998C1.3335 12.7362 1.93045 13.3332 2.66683 13.3332H13.3335C14.0699 13.3332 14.6668 12.7362 14.6668 11.9998V3.99984C14.6668 3.26346 14.0699 2.6665 13.3335 2.6665Z"
stroke={props.color ?? "currentColor"}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14.6668 4.6665L8.68683 8.4665C8.48101 8.59545 8.24304 8.66384 8.00016 8.66384C7.75728 8.66384 7.51931 8.59545 7.3135 8.4665L1.3335 4.6665"
stroke={props.color ?? "currentColor"}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" />
<rect x="2" y="4" width="20" height="16" rx="2" />
</svg>
);
};
@@ -11,6 +11,7 @@ export const WalletDotIcon: IconFC = (props) => {
viewBox="0 0 18 18"
width={props.size}
height={props.size}
style={{ color: props.color }}
role="presentation"
>
<path
@@ -0,0 +1,195 @@
"use client";
import type { ThirdwebClient } from "../../../../client/client.js";
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
import type { AuthOption } from "../../../../wallets/types.js";
import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js";
import {
iconSize,
radius,
spacing,
} from "../../../core/design-system/index.js";
import { socialIcons } from "../../../core/utils/walletIcon.js";
import { defaultAuthOptions } from "../../wallets/shared/ConnectWalletSocialOptions.js";
import { Img } from "../components/Img.js";
import { EmailIcon } from "./icons/EmailIcon.js";
import { FingerPrintIcon } from "./icons/FingerPrintIcon.js";
import { GuestIcon } from "./icons/GuestIcon.js";
import { PhoneIcon } from "./icons/PhoneIcon.js";
export function InAppWalletIcon(props: {
client: ThirdwebClient;
wallet: Wallet<"inApp">;
}) {
const enabledAuthMethods = (
props.wallet.getConfig()?.auth?.options || defaultAuthOptions
)
.slice() // clone
.sort((a, b) => {
if (a in socialIcons && !(b in socialIcons)) {
return -1;
}
if (!(a in socialIcons) && b in socialIcons) {
return 1;
}
return 0;
});
const theme = useCustomTheme();
const firstMethod = enabledAuthMethods[0];
const secondMethod = enabledAuthMethods[1];
const thirdMethod = enabledAuthMethods[2];
const fourthMethod = enabledAuthMethods[3];
const offset = "4px";
const offset2 = "6px";
const smallIconSize = "20";
const extraIconSize = "12";
if (firstMethod && secondMethod) {
return (
<div
style={{
width: `${iconSize.xl}px`,
height: `${iconSize.xl}px`,
position: "relative",
gap: spacing["3xs"],
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.md,
backgroundColor: theme.colors.tertiaryBg,
}}
>
<div
style={{
position: "absolute",
top: offset,
left: offset,
display: "flex",
}}
>
<AuthOptionIcon
authOption={firstMethod}
client={props.client}
size={smallIconSize}
/>
</div>
<div
style={{
position: "absolute",
bottom: offset,
right: offset,
display: "flex",
}}
>
<AuthOptionIcon
authOption={secondMethod}
client={props.client}
size={smallIconSize}
/>
</div>
<div>
{thirdMethod && (
<div
style={{
position: "absolute",
top: offset2,
right: offset2,
display: "flex",
}}
>
<AuthOptionIcon
authOption={thirdMethod}
client={props.client}
size={extraIconSize}
/>
</div>
)}
{fourthMethod && (
<div
style={{
position: "absolute",
bottom: offset2,
left: offset2,
display: "flex",
}}
>
<AuthOptionIcon
authOption={fourthMethod}
client={props.client}
size={extraIconSize}
/>
</div>
)}
</div>
</div>
);
}
if (firstMethod) {
return (
<div
style={{
width: `${iconSize.xl}px`,
height: `${iconSize.xl}px`,
display: "flex",
justifyContent: "center",
alignItems: "center",
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: radius.md,
backgroundColor: theme.colors.tertiaryBg,
}}
>
<AuthOptionIcon
authOption={firstMethod}
client={props.client}
key={firstMethod}
size={iconSize.lg}
/>
</div>
);
}
return null;
}
function AuthOptionIcon(props: {
authOption: AuthOption;
client: ThirdwebClient;
size: string;
}) {
const theme = useCustomTheme();
if (props.authOption in socialIcons) {
const icon = socialIcons[props.authOption as keyof typeof socialIcons];
return (
<Img
src={icon}
width={props.size}
height={props.size}
client={props.client}
/>
);
}
if (props.authOption === "phone") {
return <PhoneIcon size={props.size} color={theme.colors.secondaryText} />;
}
if (props.authOption === "email") {
return <EmailIcon size={props.size} color={theme.colors.secondaryText} />;
}
if (props.authOption === "passkey") {
return (
<FingerPrintIcon size={props.size} color={theme.colors.secondaryText} />
);
}
if (props.authOption === "guest") {
return <GuestIcon size={props.size} color={theme.colors.secondaryText} />;
}
return null;
}
@@ -61,7 +61,7 @@ export type ConnectWalletSelectUIState =
};
};
const defaultAuthOptions: AuthOption[] = [
export const defaultAuthOptions: AuthOption[] = [
"email",
"phone",
"google",
@@ -1,6 +1,7 @@
import type { Meta } from "@storybook/react";
import { ConnectButton } from "../react/web/ui/ConnectWallet/ConnectButton.js";
import { ConnectEmbed } from "../react/web/ui/ConnectWallet/Modal/ConnectEmbed.js";
import { createWallet } from "../wallets/create-wallet.js";
import { ecosystemWallet } from "../wallets/in-app/web/ecosystem.js";
import { inAppWallet } from "../wallets/in-app/web/in-app.js";
import { storyClient } from "./utils.js";
@@ -110,6 +111,60 @@ export function AllInAppWalletAuthMethods() {
);
}
export function ConfiguredInAppWalletWideModal() {
return (
<ConnectEmbed
client={storyClient}
className="foo-bar"
modalSize="wide"
wallets={[
createWallet("io.metamask"),
inAppWallet({
auth: {
options: ["google", "github", "email"],
},
}),
]}
/>
);
}
export function GoogleLoginWideModal() {
return (
<ConnectEmbed
client={storyClient}
className="foo-bar"
modalSize="wide"
wallets={[
createWallet("io.metamask"),
inAppWallet({
auth: {
options: ["google"],
},
}),
]}
/>
);
}
export function GithubLoginWideModal() {
return (
<ConnectEmbed
client={storyClient}
className="foo-bar"
modalSize="wide"
wallets={[
createWallet("io.metamask"),
inAppWallet({
auth: {
options: ["github"],
},
}),
]}
/>
);
}
export function EcosystemWallet() {
return (
<ConnectEmbed
@@ -0,0 +1,163 @@
import type { Meta } from "@storybook/react";
import {
CustomThemeProvider,
useCustomTheme,
} from "../react/core/design-system/CustomThemeProvider.js";
import { InAppWalletIcon } from "../react/web/ui/ConnectWallet/in-app-wallet-icon.js";
import { defaultAuthOptions } from "../react/web/wallets/shared/ConnectWalletSocialOptions.js";
import { inAppWallet } from "../wallets/in-app/web/in-app.js";
import type { AuthOption } from "../wallets/types.js";
import { storyClient } from "./utils.js";
const meta: Meta<typeof Variant> = {
title: "Components/in-app-wallet-icon",
decorators: [
(Story) => {
return (
<CustomThemeProvider theme="dark">
<Story />
</CustomThemeProvider>
);
},
],
};
export default meta;
function Variants() {
const theme = useCustomTheme();
return (
<div
style={{
backgroundColor: theme.colors.modalBg,
padding: "14px",
borderRadius: "10px",
display: "flex",
flexDirection: "column",
gap: "12px",
}}
>
<div>
<SectionTitle title="Default" />
<Variant authOptions={defaultAuthOptions} />
</div>
<div>
<SectionTitle title="Single method enabled" />
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
<Variant authOptions={["email"]} />
<Variant authOptions={["phone"]} />
<Variant authOptions={["passkey"]} />
<Variant authOptions={["guest"]} />
<Variant authOptions={["google"]} />
<Variant authOptions={["apple"]} />
<Variant authOptions={["facebook"]} />
<Variant authOptions={["discord"]} />
<Variant authOptions={["github"]} />
<Variant authOptions={["twitch"]} />
<Variant authOptions={["x"]} />
<Variant authOptions={["telegram"]} />
<Variant authOptions={["line"]} />
<Variant authOptions={["coinbase"]} />
<Variant authOptions={["epic"]} />
<Variant authOptions={["farcaster"]} />
<Variant authOptions={["tiktok"]} />
<Variant authOptions={["steam"]} />
</div>
</div>
<div>
<SectionTitle title="Two methods enabled" />
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
<Variant authOptions={["email", "phone"]} />
<Variant authOptions={["email", "passkey"]} />
<Variant authOptions={["email", "guest"]} />
<Variant authOptions={["email", "google"]} />
<Variant authOptions={["email", "apple"]} />
<Variant authOptions={["email", "facebook"]} />
<Variant authOptions={["google", "discord"]} />
<Variant authOptions={["google", "github"]} />
<Variant authOptions={["google", "twitch"]} />
<Variant authOptions={["google", "x"]} />
<Variant authOptions={["google", "telegram"]} />
<Variant authOptions={["google", "line"]} />
<Variant authOptions={["google", "coinbase"]} />
<Variant authOptions={["google", "epic"]} />
</div>
</div>
<div>
<SectionTitle title="Three methods enabled" />
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
<Variant authOptions={["google", "apple", "github"]} />
<Variant authOptions={["email", "phone", "guest"]} />
<Variant authOptions={["email", "phone", "google"]} />
<Variant authOptions={["email", "phone", "apple"]} />
<Variant authOptions={["email", "phone", "facebook"]} />
</div>
</div>
<div>
<SectionTitle title="Four or more methods enabled" />
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
<Variant
authOptions={["email", "phone", "google", "apple", "facebook"]}
/>
<Variant authOptions={["epic", "tiktok", "github", "email"]} />
<Variant
authOptions={["twitch", "tiktok", "epic", "email", "phone"]}
/>
<Variant authOptions={["email", "phone", "passkey", "guest"]} />
<Variant
authOptions={[
"google",
"apple",
"facebook",
"github",
"discord",
"twitch",
"x",
]}
/>
</div>
</div>
</div>
);
}
export function LightTheme() {
return <ThemeSetup theme="light" />;
}
export function DarkTheme() {
return <ThemeSetup theme="dark" />;
}
function ThemeSetup(props: { theme: "light" | "dark" }) {
return (
<CustomThemeProvider theme={props.theme}>
<Variants />
</CustomThemeProvider>
);
}
function Variant(props: { authOptions: AuthOption[] }) {
return (
<InAppWalletIcon
client={storyClient}
wallet={inAppWallet({
auth: {
options: props.authOptions,
},
})}
/>
);
}
function SectionTitle(props: { title: string }) {
const theme = useCustomTheme();
return (
<p style={{ color: theme.colors.secondaryText, fontSize: "14px" }}>
{props.title}
</p>
);
}