[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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user