diff --git a/packages/app/src/systems/Core/components/InputSecurePassword/InputSecurePassword.tsx b/packages/app/src/systems/Core/components/InputSecurePassword/InputSecurePassword.tsx index afd5ac874d..185cbe2677 100644 --- a/packages/app/src/systems/Core/components/InputSecurePassword/InputSecurePassword.tsx +++ b/packages/app/src/systems/Core/components/InputSecurePassword/InputSecurePassword.tsx @@ -1,16 +1,10 @@ -import type { ThemeUtilsCSS } from '@fuel-ui/css'; -import { cssObj } from '@fuel-ui/css'; -import type { InputPasswordProps } from '@fuel-ui/react'; -import { - Box, - Icon, - InputPassword, - PasswordStrength, - usePasswordStrength, -} from '@fuel-ui/react'; -import unsafeList from '@fuel-ui/react/unsafe-passwords'; -import { useEffect, useState } from 'react'; -import type { ControllerRenderProps, FieldValues } from 'react-hook-form'; +import type { ThemeUtilsCSS } from "@fuel-ui/css"; +import { cssObj } from "@fuel-ui/css"; +import type { InputPasswordProps } from "@fuel-ui/react"; +import { Box, Icon, InputPassword } from "@fuel-ui/react"; +import { useEffect, useState } from "react"; +import React from "react"; +import type { ControllerRenderProps, FieldValues } from "react-hook-form"; export type InputSecurePasswordProps = { onChangeStrength?: (strength: string) => void; @@ -25,51 +19,128 @@ export type InputSecurePasswordProps = { css?: ThemeUtilsCSS; }; +const calculatePasswordStrength = (password: string): string => { + let score = 0; + + if (password.length >= 8) score += 1; + if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score += 1; + if (/\d/.test(password)) score += 1; + if (/[@$!%*?&#^_-]/.test(password)) score += 1; + + if (score <= 1) return "weak"; + if (score === 2) return "average"; + if (score >= 3) return "strong"; + + return "weak"; +}; + export function InputSecurePassword({ inputProps, field, onChangeStrength, onChange, onBlur, - placeholder = 'Type your password', - ariaLabel = 'Your Password', + placeholder = "Type your password", + ariaLabel = "Your Password", css, }: InputSecurePasswordProps) { const [passwordTooltipOpened, setPasswordTooltipOpened] = useState(false); - const password = field.value || ''; - const { strength } = usePasswordStrength({ - minLength: 8, - password, - unsafeList, - }); + const [strength, setStrength] = useState("weak"); + const password = field.value || ""; - // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { - onChangeStrength?.(strength); - }, [strength]); + const currentStrength = calculatePasswordStrength(password); + setStrength(currentStrength); + onChangeStrength?.(currentStrength); + }, [password, onChangeStrength]); + + const getStrengthColor = (level: string, index: number) => { + if (level === "weak") { + return index === 0 ? "red" : "gray"; + } + if (level === "average") { + return index === 0 || index === 1 ? "yellow" : "gray"; + } + if (level === "strong") { + return "green"; + } + return "gray"; // Default to gray for any unexpected case + }; return ( - - + {[...Array(3)].map((_, index) => ( + + ))} + + + {/* Tooltip Wrapper */} + setPasswordTooltipOpened(true)} onMouseLeave={() => setPasswordTooltipOpened(false)} - aria-label="Password strength" > - - - - + + {passwordTooltipOpened && ( + + + + {strength.charAt(0).toUpperCase() + strength.slice(1)} + + + + {[...Array(3)].map((_, index) => ( + + ))} + + + +

A secure password should have:

+
    +
  • {password.length >= 8 ? "✓" : "✗"} Min. 8 characters
  • +
  • + {/[a-z]/.test(password) && /[A-Z]/.test(password) + ? "✓" + : "✗"}{" "} + Upper & lower case letters +
  • +
  • {/\d/.test(password) ? "✓" : "✗"} Numbers
  • +
  • + {/[@$!%*?&#^_-]/.test(password) ? "✓" : "✗"} Special + characters +
  • +
+
+
+ )} +
+ + (""); + const [userClicked, setUserClicked] = useState(false); + + // Update the current hash based on window.location.hash on mount + useEffect(() => { + if (typeof window !== "undefined") { + setCurrentHash(window.location.hash); + } + }, []); + + // Track hash changes when a user clicks on a link + const handleClick = (hash: string) => { + setUserClicked(true); // Indicate that the change is user-initiated + setCurrentHash(hash); // Update the current hash + setTimeout(() => setUserClicked(false), 1000); // Reset after a short delay + }; + + // Scroll observer for automatic highlighting during scrolling + useEffect(() => { + if (typeof window === "undefined") return; + + const observerOptions = { + root: null, // Use the viewport + rootMargin: "0px", + threshold: Array.from({ length: 11 }, (_, i) => i / 10), // Threshold from 0.0 to 1.0 + }; + + const sectionVisibility = new Map(); + + const observer = new IntersectionObserver((entries) => { + if (userClicked) return; // Skip if the user clicked a link recently + + entries.forEach((entry) => { + sectionVisibility.set(entry.target.id, entry.intersectionRatio); + }); + + // Find the section with the highest visibility ratio + let mostVisibleId = ""; + let maxVisibility = 0; + sectionVisibility.forEach((visibility, id) => { + if (visibility > maxVisibility) { + mostVisibleId = id; + maxVisibility = visibility; + } + }); + + if (mostVisibleId) { + setCurrentHash(`#${mostVisibleId}`); + } + }, observerOptions); + + // Flatten main headings and subheadings into a single list + const allSections: any = []; + headings.forEach((heading) => { + allSections.push(heading); + if (heading.children) { + allSections.push(...heading.children); + } + }); + + const sections = allSections.map((heading: any) => + document.getElementById(heading.id) + ); + + sections.forEach((section: any) => section && observer.observe(section)); + + return () => { + sections.forEach( + (section: any) => section && observer.unobserve(section) + ); + }; + }, [headings, userClicked]); + return ( @@ -12,12 +87,32 @@ export function TableOfContent() { {headings.map((heading) => ( - {heading.title} + handleClick(`#${heading.id}`)} // Handle click events + style={ + currentHash === `#${heading.id}` + ? { color: "white", fontWeight: "bold" } + : {} + } + > + {heading.title} + {heading.children && ( - {heading.children.map((heading) => ( - - {heading.title} + {heading.children.map((subHeading) => ( + + handleClick(`#${subHeading.id}`)} // Handle click events + style={ + currentHash === `#${subHeading.id}` + ? { color: "white", fontWeight: "bold" } + : {} + } + > + {subHeading.title} + ))} @@ -30,7 +125,7 @@ export function TableOfContent() { isExternal href="https://github.com/fuellabs/fuels-wallet/issues/new/choose" > - Questions? Give us a feedback + Questions? Give us feedback Edit this page @@ -41,52 +136,52 @@ export function TableOfContent() { ); } -const LIST_ITEM = '.fuel_List > .fuel_ListItem'; +const LIST_ITEM = ".fuel_List > .fuel_ListItem"; const styles = { queries: cssObj({ - display: 'none', + display: "none", - '@xl': { - display: 'block', + "@xl": { + display: "block", }, }), root: cssObj({ - position: 'sticky', + position: "sticky", top: 0, - py: '$8', - pr: '$8', + py: "$8", + pr: "$8", h6: { mt: 0, }, [LIST_ITEM]: { - pb: '$2', + pb: "$2", a: { - fontWeight: '$normal', - color: '$intentsBase11', + fontWeight: "$normal", + color: "$intentsBase11", }, }, [`${LIST_ITEM} > ${LIST_ITEM}:nth-child(1)`]: { - pt: '$2', + pt: "$2", }, [`${LIST_ITEM} > ${LIST_ITEM}`]: { a: { - fontWeight: '$normal', - color: '$intentsBase9', + fontWeight: "$normal", + color: "$intentsBase9", }, }, }), feedback: cssObj({ - display: 'flex', - flexDirection: 'column', - pt: '$3', - borderTop: '1px solid $border', - fontSize: '$sm', - - 'a, a:visited': { - color: '$intentsBase10', + display: "flex", + flexDirection: "column", + pt: "$3", + borderTop: "1px solid $border", + fontSize: "$sm", + + "a, a:visited": { + color: "$intentsBase10", }, }), };