From 8e079da0ff7540c68804ce960ee41fbffa79d33e Mon Sep 17 00:00:00 2001 From: Guru Date: Wed, 11 Dec 2024 02:57:06 +0530 Subject: [PATCH] feat: functional cards --- .../src/components/Avatar.tsx | 60 ++++++ .../src/components/DocDetails.tsx | 51 +++++ .../src/components/LoginCard.tsx | 86 ++++++++ .../src/components/LoginForm.tsx | 156 ++++++++++++++ .../src/components/MFACard.tsx | 127 ++++++++++++ .../src/components/PasskeyCard.tsx | 121 +++++++++++ .../src/components/UserCard.tsx | 192 ++++++++++++++++++ .../src/components/useSocialLogin.tsx | 25 +++ 8 files changed, 818 insertions(+) create mode 100644 demo/redirect-flow-example/src/components/Avatar.tsx create mode 100644 demo/redirect-flow-example/src/components/DocDetails.tsx create mode 100644 demo/redirect-flow-example/src/components/LoginCard.tsx create mode 100644 demo/redirect-flow-example/src/components/LoginForm.tsx create mode 100644 demo/redirect-flow-example/src/components/MFACard.tsx create mode 100644 demo/redirect-flow-example/src/components/PasskeyCard.tsx create mode 100644 demo/redirect-flow-example/src/components/UserCard.tsx create mode 100644 demo/redirect-flow-example/src/components/useSocialLogin.tsx diff --git a/demo/redirect-flow-example/src/components/Avatar.tsx b/demo/redirect-flow-example/src/components/Avatar.tsx new file mode 100644 index 0000000..3063b35 --- /dev/null +++ b/demo/redirect-flow-example/src/components/Avatar.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "./Card"; + +const avatarVariants = cva( + `relative inline-flex items-center justify-center overflow-hidden bg-app-gray-100 dark:bg-app-gray-600`, + { + variants: { + size: { + xs: "w-6 h-6", + sm: "w-8 h-8", + md: "w-10 h-10", + lg: "w-14 h-14", + xl: "w-20 h-20", + }, + rounded: { + true: "rounded-full", + false: "rounded-lg", + }, + border: { + true: "p-1 ring-2 ring-app-gray-300 dark:ring-app-gray-500", + false: "", + }, + }, + defaultVariants: { + size: "md", + rounded: true, + border: false, + }, + }, +); + +export interface AvatarProps + extends React.HTMLAttributes, + VariantProps { + children: React.ReactNode; + id?: string; + classes?: Partial>; +} + +const Avatar = React.forwardRef( + ({ className, size, rounded, border, children, id, classes = {}, ...props }, ref) => { + return ( +
+ {children} +
+ ); + }, +); + +Avatar.displayName = "Avatar"; + +export { Avatar, avatarVariants }; \ No newline at end of file diff --git a/demo/redirect-flow-example/src/components/DocDetails.tsx b/demo/redirect-flow-example/src/components/DocDetails.tsx new file mode 100644 index 0000000..129a9e4 --- /dev/null +++ b/demo/redirect-flow-example/src/components/DocDetails.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { Button } from "./Button"; +import { Card } from "./Card"; +import { Link } from "./Link"; + +const DocDetails: React.FC = () => { + return ( + +
+

+ Experience Web3Auth, first hand +

+

+ Browse our full suite of features for your dApp with our docs. Access code examples for + these features by visiting our{" "} + + playground + + . +

+
+
+ + +
+
+ ); +}; + +export { DocDetails }; \ No newline at end of file diff --git a/demo/redirect-flow-example/src/components/LoginCard.tsx b/demo/redirect-flow-example/src/components/LoginCard.tsx new file mode 100644 index 0000000..d0964c5 --- /dev/null +++ b/demo/redirect-flow-example/src/components/LoginCard.tsx @@ -0,0 +1,86 @@ +import * as React from "react"; +import { Card } from "./Card"; +import { Icon } from "./Icon"; +import { TextField } from "./TextField"; +import { Button } from "./Button"; +import { LoginForm } from "./LoginForm"; +import { useSocialLogins } from "./useSocialLogin"; +import { SocialLoginObj } from "./types"; + +interface LoginCardProps { + handleEmailPasswordLess: () => void; + handleSocialLogin: (item: SocialLoginObj) => void; +} + +const LoginCard: React.FC = ({handleEmailPasswordLess, handleSocialLogin}) => { + const [loginHint, setLoginHint] = React.useState(""); + + const socialLogins = useSocialLogins(); + + const handlePasswordlessLogin = (e: React.FormEvent) => { + e.preventDefault(); + console.log("loginHint", loginHint); + // Handle passwordless login + handleEmailPasswordLess(); + }; + + const handleSocial = (item: SocialLoginObj, index: number) => { + // Handle social login + handleSocialLogin(item); + }; + + return ( +
+ +
+
+ +
+

Welcome to Web3Auth

+

Login to continue

+
+ +
+ setLoginHint(e.target.value)} + label="Email or Phone" + pill={true} + type="email" + className="w-full" + required + placeholder="E.g. +00-123455/name@example.com" + /> + + +
+
+ web3auth logo + web3auth logo +
+
+
+ ); +}; + +export { LoginCard }; diff --git a/demo/redirect-flow-example/src/components/LoginForm.tsx b/demo/redirect-flow-example/src/components/LoginForm.tsx new file mode 100644 index 0000000..0357cc7 --- /dev/null +++ b/demo/redirect-flow-example/src/components/LoginForm.tsx @@ -0,0 +1,156 @@ +import * as React from "react"; +import { useState } from "react"; +import { Button } from "./Button"; +import { Icon } from "./Icon"; +import { TextField } from "./TextField"; +import { cn } from "./Card"; +import { Image } from "./Image"; +import { LoginFormProps } from "./types"; + +const LoginForm: React.FC = ({ + socialLogins = [], + pill = false, + expandLabel = "View More", + collapseLabel = "View Less", + socialLoginMessage = "We do not store any data related to your social logins.", + showInput = true, + inputBtnLabel = "", + inputProps = {}, + inputExpandBtnProps = {}, + gridCols = 3, + inputBtnProps = {}, + primaryBtn = "social", + classes = {}, + isExistingLogin = false, + existingLoginProps = {}, + existingLabelInline = false, + divider = false, + onSocialLoginClick, + onExistingLoginClick, + onInputBtnClick, + children, +}) => { + const [viewMoreOptions, setViewMoreOptions] = useState(false); + const [imagesLoaded, setImagesLoaded] = useState(false); + + const getIcon = (provider: string) => { + if (provider === "twitter") { + return "twitter-x"; + } + return provider; + }; + + const toggleViewMoreOptions = () => { + setViewMoreOptions(!viewMoreOptions); + if (!imagesLoaded) { + setImagesLoaded(true); + } + }; + + const isHiddenIcon = (index: number) => { + if (imagesLoaded) return false; + return !viewMoreOptions && index > 3; + }; + + return ( +
+ {isExistingLogin && existingLoginProps?.verifier && ( + + )} + {socialLogins.length > 0 && ( +
+ {socialLogins.map((item, index) => ( +
+ +
+ ))} +
+ )} + {socialLoginMessage && ( +

+ {socialLoginMessage} +

+ )} + {socialLogins.length > 0 && socialLogins.length > 4 && ( +
+ +
+ )} + {divider && ( +
+
+ or +
+
+ )} + {children} + {showInput && ( + <> + + + + )} +
+ ); +}; + +export { LoginForm }; \ No newline at end of file diff --git a/demo/redirect-flow-example/src/components/MFACard.tsx b/demo/redirect-flow-example/src/components/MFACard.tsx new file mode 100644 index 0000000..e16958d --- /dev/null +++ b/demo/redirect-flow-example/src/components/MFACard.tsx @@ -0,0 +1,127 @@ +import * as React from "react"; +import { Button } from "./Button"; +import { Card } from "./Card"; +import { AddShareType, useCoreKit } from "../composibles/useCoreKit"; +import { BN } from "bn.js"; +import { HiOutlineMail } from "react-icons/hi"; + +const FACTOR_MAP: Record = { + device: { title: "Device", icon: "mobile-icon" }, + seedPhrase: { title: "Recovery Phrase", icon: "key-solid-icon" }, + social: { title: "Social Recovery Factor", icon: "key-solid-icon" }, + password: { title: "Password", icon: "key-solid-icon" }, +}; + + +const shareDetails = [ + // Example share details + { shareType: "device", details: "Chrome 91 (Windows)" }, + { shareType: "seedPhrase", details: "Recovery Phrase" }, +].map((share) => ({ + title: FACTOR_MAP[share.shareType]?.title || "", + details: share.details, + icon: FACTOR_MAP[share.shareType]?.icon || "", +})); + +const MfaCard: React.FC = () => { + const { setAddShareType, coreKitInstance } = useCoreKit(); + const [userInfo, setUserInfo] = React.useState({}); + + React.useEffect(() => { + const fetchUserInfo = async () => { + if (!coreKitInstance) { + return; + } + const userInfo = await coreKitInstance.getUserInfo(); + setUserInfo(userInfo); + }; + fetchUserInfo(); + }, [coreKitInstance]); + + const addMfa = (addShareType: AddShareType) => { + console.log("Add MFA"); + setAddShareType(addShareType); + }; + + const criticalReset = async () => { + if (!coreKitInstance) { + throw new Error("coreKitInstance is not set"); + } + //@ts-ignore + // if (selectedNetwork === WEB3AUTH_NETWORK.MAINNET) { + // throw new Error("reset account is not recommended on mainnet"); + // } + await coreKitInstance.tKey.storageLayer.setMetadata({ + privKey: new BN(coreKitInstance.state.postBoxKey!, "hex"), + input: { message: "KEY_NOT_FOUND" }, + }); + } + + return ( + +
+
+

MFA

+ {/* + {userInfo.isMfaEnabled ? "Enabled" : "Disabled"} + */} +
+

+ Add an additional security layer to your wallets. While enabled, you will need to verify another factor when logging in. +

+
+ +
+ + + + +
+ +
+ +
+
+
+ {["email_passwordless", "jwt"].includes(userInfo.typeOfLogin) ? ( + + ) : ( + {`${userInfo.typeOfLogin} + )} +
+
+

{userInfo.typeOfLogin}

+

{userInfo.verifierId}

+
+
+ { + // shareDetails.map((shareDetail) => ( + //
+ //
+ // {/* */} + //
+ //
+ //

{shareDetail.title}

+ //

{shareDetail.details}

+ //
+ //
+ // )) + } +
+
+ ); +}; + +export { MfaCard }; diff --git a/demo/redirect-flow-example/src/components/PasskeyCard.tsx b/demo/redirect-flow-example/src/components/PasskeyCard.tsx new file mode 100644 index 0000000..f4d110e --- /dev/null +++ b/demo/redirect-flow-example/src/components/PasskeyCard.tsx @@ -0,0 +1,121 @@ +import * as React from "react"; +import { Button } from "./Button"; +import { Card } from "./Card"; +import { useCoreKit } from "../composibles/useCoreKit"; +import { shouldSupportPasskey } from "../App"; +import { HiOutlineMinusCircle } from "react-icons/hi"; +import { HiOutlineKey } from "react-icons/hi"; + +const PasskeysCard: React.FC = () => { + const { passkeyPlugin, coreKitInstance } = useCoreKit(); + const [hasPasskeys, setHasPasskeys] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(false); + interface Passkey { + verifier_id: string; + verifier: string; + passkey_pub_key: string; + label: string; + } + + const [passkeys, setPasskeys] = React.useState([]); + + React.useEffect(() => { + listPasskeys(); + }, []); + + React.useEffect(() => { + if (passkeys.length > 0) { + setHasPasskeys(true); + } + }, [passkeys]); + + const deletePasskey = (id: string) => { + console.log("delete passkey", id); + passkeyPlugin?.unRegisterPasskey({ credentialPubKey: id, verifier: "web3auth" } as any); + }; + + const registerPasskey = async () => { + setIsLoading(true); + try { + if (!coreKitInstance) { + throw new Error("coreKitInstance is not set"); + } + if (!passkeyPlugin) { + throw new Error("passkeyPlugin is not set"); + } + const result = shouldSupportPasskey(); + if (!result.isBrowserSupported) { + console.log("Browser not supported"); + throw new Error("Browser not supported"); + } + await passkeyPlugin.registerPasskey(); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }; + + const listPasskeys = async () => { + try { + if (!coreKitInstance) { + throw new Error("coreKitInstance is not set"); + } + if (!passkeyPlugin) { + throw new Error("passkeyPlugin is not set"); + } + const passkeys = await passkeyPlugin.listPasskeys(); + console.log({ passkeyPlugin, passkeys }); + setPasskeys(passkeys); + } catch (error) { + console.error(error); + } + }; + + return ( + +
+
+

Passkeys

+
+

Link a passkey to your account

+
+ + + + {hasPasskeys &&
} + + {hasPasskeys && ( +
+ {passkeys.map((passkey) => ( +
+
+ +
+
+

{passkey.label}

+

{passkey.verifier_id}

+
+
+ +
+
+ ))} +
+ )} +
+ ); +}; + +export { PasskeysCard }; diff --git a/demo/redirect-flow-example/src/components/UserCard.tsx b/demo/redirect-flow-example/src/components/UserCard.tsx new file mode 100644 index 0000000..c082985 --- /dev/null +++ b/demo/redirect-flow-example/src/components/UserCard.tsx @@ -0,0 +1,192 @@ +import * as React from "react"; +import { Avatar } from "./Avatar"; +import { Button } from "./Button"; +import { Card } from "./Card"; +import { Drawer } from "./Drawer"; +import { useCoreKit } from "../composibles/useCoreKit"; +import { COREKIT_STATUS, factorKeyCurve } from "@web3auth/mpc-core-kit"; +import { HiOutlineDuplicate } from "react-icons/hi"; +import { HiOutlineCheckCircle } from "react-icons/hi"; +import { Link } from "./Link"; + +const UserCard: React.FC = () => { + const { coreKitInstance, web3, coreKitStatus, drawerHeading, setDrawerHeading, drawerInfo, setDrawerInfo } = useCoreKit(); + + const [openConsole, setOpenConsole] = React.useState(false); + + const [isCopied, setIsCopied] = React.useState(false); + const [userInfo, setUserInfo] = React.useState(null); + const [account, setAccount] = React.useState(""); + const [imageError, setImageError] = React.useState(false); + const [currentDrawerHeading, setCurrentDrawerHeading] = React.useState(""); + const [currentDrawerInfo, setCurrentDrawerInfo] = React.useState(null); + + React.useEffect(() => { + if (drawerHeading) { + setCurrentDrawerHeading(drawerHeading); + setDrawerHeading(""); + setOpenConsole(true); + } + }, [drawerHeading]); + + React.useEffect(() => { + if (drawerInfo) { + setCurrentDrawerInfo(drawerInfo); + setDrawerInfo(""); + setOpenConsole(true); + } + }, [drawerInfo]); + + const listFactors = async () => { + if (!coreKitInstance) { + throw new Error("coreKitInstance not found"); + } + const temp = await coreKitInstance.getDeviceFactor(); + // const temp2 = await coreKitInstance.tKey.reconstructKey(); + const factorPubs = coreKitInstance.tKey.metadata.factorPubs; + if (!factorPubs) { + throw new Error("factorPubs not found"); + } + const pubsHex = factorPubs[coreKitInstance.tKey.tssTag].map((pub) => { + return pub.toSEC1(factorKeyCurve, true).toString("hex"); + }); + console.log(pubsHex); + coreKitInstance.tKey.getMetadata().getGeneralStoreDomain("tssSecurityQuestion:default"); + }; + + const getUserInfo = (): void => { + const user = coreKitInstance?.getUserInfo(); + console.log("User Info: ", user); + listFactors(); + if (user) setUserInfo(user); + }; + + const getAccounts = async () => { + if (!web3) { + return; + } + const address = (await web3.eth.getAccounts())[0]; + setAccount(address); + return address; + }; + + React.useEffect(() => { + getAccounts(); + }, [web3]); + + React.useEffect(() => { + if (coreKitStatus === COREKIT_STATUS.LOGGED_IN) { + getUserInfo(); + } + }, [coreKitStatus]); + + const handleConsoleBtn = () => { + setDrawerHeading("User Info Console"); + setDrawerInfo(userInfo); + setOpenConsole(true); + }; + + const handleCopyAddress = () => { + setIsCopied(true); + navigator.clipboard.writeText(account); + setTimeout(() => { + setIsCopied(false); + }, 1000); + }; + + const getTruncateString = (val: string) => { + const address = val || ""; + return `${address.slice(0, 10)}....${address.slice(address.length - 6)}`; + }; + + const returnAvatarLetter = (name: string) => { + if (!name) return "W3A"; + if (name.includes("@")) { + return `${name.charAt(0).toUpperCase()}${name.charAt(1).toUpperCase()}`; + } else { + const [nameFirst, nameSecond] = name.split(" "); + return `${nameFirst?.charAt(0).toUpperCase() || ""}${nameSecond?.charAt(0).toUpperCase() || ""}`; + } + }; + + return ( + + {userInfo ? ( + <> + + {userInfo.profileImage && !imageError ? ( + Profile { + setImageError(true); + }} + /> + ) : ( + {returnAvatarLetter(userInfo.name)} + )} + +
+

{userInfo.name}

+

{userInfo.email ? userInfo.email : userInfo.name}

+ +
+
+
+ +
+ setOpenConsole(false)} + > +
+

{currentDrawerHeading}

+
+
+                  {JSON.stringify(currentDrawerInfo, null, 2)}
+                
+
+ +
+
+ + ) : ( + <> + )} +
+ ); +}; + +export { UserCard }; diff --git a/demo/redirect-flow-example/src/components/useSocialLogin.tsx b/demo/redirect-flow-example/src/components/useSocialLogin.tsx new file mode 100644 index 0000000..d91fce8 --- /dev/null +++ b/demo/redirect-flow-example/src/components/useSocialLogin.tsx @@ -0,0 +1,25 @@ +import { useMemo } from "react"; +import { LOGIN_PROVIDER, SocialLoginObj } from "./types"; + +const useSocialLogins = (): SocialLoginObj[] => { + + const socialLoginsAll = useMemo((): SocialLoginObj[] => { + const loginProviders = Object.values(LOGIN_PROVIDER).filter( + (x) => + x !== LOGIN_PROVIDER.EMAIL_PASSWORDLESS && + x !== LOGIN_PROVIDER.SMS_PASSWORDLESS && + x !== LOGIN_PROVIDER.WEIBO && + x !== LOGIN_PROVIDER.WEBAUTHN && + x !== LOGIN_PROVIDER.JWT + ); + return loginProviders.map((loginProvider) => ({ + description: loginProvider === LOGIN_PROVIDER.GOOGLE ? "Continue with Google" : "", + icon: loginProvider, + verifier: loginProvider, + })); + }, []); + + return socialLoginsAll; +}; + +export { useSocialLogins }; \ No newline at end of file