From c58a118cb5ea226963aa9595825c15714464ca84 Mon Sep 17 00:00:00 2001 From: KaffinPX Date: Fri, 18 Oct 2024 01:01:35 +0300 Subject: [PATCH] Creation logic (mnemonic validation missing) --- _locales/en/messages.json | 260 -------------------------------- index.html | 8 +- src/App.tsx | 3 + src/contexts/Settings.tsx | 6 +- src/main.tsx | 2 + src/pages/Creation.tsx | 36 +++-- src/pages/Creation/Create.tsx | 39 +++++ src/pages/Creation/Import.tsx | 61 ++++++++ src/pages/Creation/Landing.tsx | 19 +++ src/pages/Creation/Password.tsx | 84 +++++++++++ src/style.css | 13 ++ tailwind.config.js | 76 ++-------- 12 files changed, 251 insertions(+), 356 deletions(-) delete mode 100644 _locales/en/messages.json create mode 100644 src/style.css diff --git a/_locales/en/messages.json b/_locales/en/messages.json deleted file mode 100644 index d5c4fbd..0000000 --- a/_locales/en/messages.json +++ /dev/null @@ -1,260 +0,0 @@ -{ - "URL": { - "message": "URL" - }, - "account": { - "message": "Account" - }, - "add": { - "message": "Add" - }, - "addNode": { - "message": "Add Node" - }, - "addNodeAddress": { - "message": "Address (ws:// or wss://)" - }, - "addNodeDescription": { - "message": "Be careful as 3. party nodes may track your actvitity." - }, - "address": { - "message": "Address" - }, - "addressChange": { - "message": "Change addresses" - }, - "addressDescription": { - "message": "It counts how many addresses you've created." - }, - "addressReceive": { - "message": "Receive addresses" - }, - "addressTitle": { - "message": "Address Index" - }, - "amount": { - "message": "Amount" - }, - "available": { - "message": "Available" - }, - "backupMnemonic": { - "message": "It's time to save your mnemonic phrase. Write it down and keep it safe." - }, - "backupWallet": { - "message": "Backup Wallet" - }, - "cancel": { - "message": "Cancel" - }, - "confirm": { - "message": "Confirm" - }, - "confirmBackup": { - "message": "I've saved my mnemonic phrase in a safe place" - }, - "confirmationDescription": { - "message": "Review the details of the transaction before signing it" - }, - "connect": { - "message": "Connect" - }, - "connected": { - "message": "Connected" - }, - "connection": { - "message": "Connection" - }, - "connectionDescription": { - "message": "The website is requesting access to your wallet APIs." - }, - "continue": { - "message": "Continue" - }, - "createIntro": { - "message": "Create a wallet to get started" - }, - "createWallet": { - "message": "Create Wallet" - }, - "currency": { - "message": "Currency" - }, - "currencyDescription": { - "message": "Change preferred exchange currency of wallet." - }, - "disconnected": { - "message": "Disconnected" - }, - "export": { - "message": "Export" - }, - "exportButton": { - "message": "Export wallet" - }, - "exportDescription": { - "message": "Remember its your responsibility to keep your mnemonic safe." - }, - "exportTitle": { - "message": "Export Mnemonic" - }, - "fee": { - "message": "Fee" - }, - "finish": { - "message": "Finish" - }, - "general": { - "message": "General" - }, - "hide": { - "message": "Hide" - }, - "immutability": { - "message": "Always verify the address you are sending to. If you send to the wrong address, your funds will be lost forever." - }, - "import": { - "message": "Import" - }, - "importDescription": { - "message": "Enter your 24-word mnemonic phrase below to import your wallet" - }, - "importIntro": { - "message": "Already have a wallet? Import it here" - }, - "importWallet": { - "message": "Import Wallet" - }, - "kaspianIntro": { - "message": "Welcome to Kaspian. Let's get you started!" - }, - "mnemonic": { - "message": "Mnemonic phrase" - }, - "mnemonicDescription": { - "message": "Backup the only way to import your wallet again." - }, - "name": { - "message": "Name" - }, - "next": { - "message": "Next" - }, - "node": { - "message": "Node" - }, - "nodeDescription": { - "message": "Select the node you will use to interact with network" - }, - "password": { - "message": "Password" - }, - "passwordDescription": { - "message": "Set a password, this password will be used to encrypt your mnemonic." - }, - "passwordLowerCase": { - "message": "At least one lowercase character." - }, - "passwordNumber": { - "message": "At least one number." - }, - "passwordSpecialCharacter": { - "message": "At least one special character." - }, - "passwordTooShort": { - "message": "At least 8 characters long." - }, - "passwordUpperCase": { - "message": "At least one uppercase character." - }, - "receive": { - "message": "Receive" - }, - "receiveDescription": { - "message": "Scan the QR code to receive payment details." - }, - "resetButton": { - "message": "Reset wallet" - }, - "resetDescription": { - "message": "This action cannot be undone and Kaspian cant help you to get your wallet back." - }, - "resetTitle": { - "message": "Are you absolutely sure?" - }, - "review": { - "message": "Review Transaction" - }, - "rules": { - "message": "This journey will be exciting, but you have to follow some rules." - }, - "scan": { - "message": "Scan" - }, - "selfCustody": { - "message": "Your wallet is stored locally on your device. If you lose access to your device, or forget the password, you won't be able to recover your wallet without your mnemonic phrase." - }, - "send": { - "message": "Send" - }, - "sendDescription": { - "message": "Enter the amount and the address to send Kaspa" - }, - "sendTitle": { - "message": "Send Kaspa" - }, - "setPassword": { - "message": "Set Password" - }, - "settings": { - "message": "Settings" - }, - "shareMnemonic": { - "message": "Never share your mnemonic phrase" - }, - "show": { - "message": "Show" - }, - "sign": { - "message": "Sign" - }, - "submit": { - "message": "Submit" - }, - "submitDescription": { - "message": "Submit your signed transaction(s) to network, miners should add it to a block." - }, - "submitTitle": { - "message": "Submit Transaction" - }, - "success": { - "message": "Success" - }, - "theme": { - "message": "Theme" - }, - "themeDescription": { - "message": "Select the theme of wallet" - }, - "unlock": { - "message": "Unlock" - }, - "unlockIntro": { - "message": "We're happy you're back! Please enter your password to unlock your wallet" - }, - "verifyAddresses": { - "message": "Verify the addresses you send to" - }, - "viewOnExplorer": { - "message": "View it on the explorer" - }, - "wallet": { - "message": "Wallet" - }, - "walletAccess": { - "message": "Your mnemonic phrase is the key to your wallet. Anyone who has access to it can steal your funds." - }, - "welcoming": { - "message": "It's about to get exciting. You're all set up and ready to go!" - } -} \ No newline at end of file diff --git a/index.html b/index.html index dc2f788..edba9f8 100644 --- a/index.html +++ b/index.html @@ -1,22 +1,16 @@ - + Kaspian - diff --git a/src/App.tsx b/src/App.tsx index cc37810..a1adff0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,8 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom' import { SettingsProvider } from './contexts/Settings' import { KaspaProvider } from './contexts/Kaspa' import Landing from './pages/Landing' +import Creation from './pages/Creation' + function App () { return ( @@ -9,6 +11,7 @@ function App () { } /> + } /> diff --git a/src/contexts/Settings.tsx b/src/contexts/Settings.tsx index a9d2893..f004a5a 100644 --- a/src/contexts/Settings.tsx +++ b/src/contexts/Settings.tsx @@ -53,7 +53,7 @@ export function SettingsProvider({ children }: { LocalStorage.set("settings", settings) }, [ settings ]) - useEffect(() => { + useEffect(() => { // TODO: Migrate to daisyUI const root = window.document.documentElement root.classList.remove("light", "dark") @@ -64,11 +64,11 @@ export function SettingsProvider({ children }: { ? "dark" : "light" - root.classList.add(systemTheme) + // root.classList.add(systemTheme) return } - root.classList.add(settings['theme']) + // root.classList.add(settings['theme']) }, [ settings['theme'] ]) const load = useCallback(async () => { diff --git a/src/main.tsx b/src/main.tsx index 2cee5ed..98218f6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,8 @@ import ReactDOM from 'react-dom/client' import App from './App' +import './style.css' + ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ) diff --git a/src/pages/Creation.tsx b/src/pages/Creation.tsx index c03bc9b..fc7cf60 100644 --- a/src/pages/Creation.tsx +++ b/src/pages/Creation.tsx @@ -1,10 +1,10 @@ import { useState } from "react" import { useNavigate } from "react-router-dom" +import useKaspa from "@/hooks/useKaspa" import Landing from "@/pages/Creation/Landing" import Create from "@/pages/Creation/Create" import Password from "@/pages/Creation/Password" -import Import from "@/pages/Creation/Import" -import useKaspa from "@/hooks/useKaspa" +import Import from "@/pages/Creation/Import" export enum Tabs { Landing, @@ -13,37 +13,35 @@ export enum Tabs { Password } -export default function CreateWallet () { +export default function Creation () { const navigate = useNavigate() const { request } = useKaspa() const [ tab, setTab ] = useState(Tabs.Landing) - const [ sensitive, setSensitive ] = useState("") - const [ isImport, setIsImport ] = useState(false) + const [ mnemonic, setMnemonic ] = useState("") return ( { [ Tabs.Landing ]: { - if (tab === Tabs.Password) setIsImport(true) setTab(tab) }} />, - [ Tabs.Password ]: { - if (isImport) { - setSensitive(password) - setTab(Tabs.Import) + [ Tabs.Import ]: { + setMnemonic(mnemonic) + setTab(Tabs.Password) + }} />, + [ Tabs.Password ]: { + if (mnemonic) { + await request('wallet:import', [ mnemonic, password ]) + + navigate('/wallet') } else { const mnemonic = await request('wallet:create', [ password ]) - - setSensitive(mnemonic) + + setMnemonic(mnemonic) setTab(Tabs.Create) } }} />, - [ Tabs.Import ]: { - await request('wallet:import', [ mnemonic, sensitive ]) - - navigate('/wallet') - }} />, - [ Tabs.Create ]: { navigate('/wallet') }} /> - }[tab] + [ Tabs.Create ]: navigate('/wallet')} /> + }[ tab ] ) } \ No newline at end of file diff --git a/src/pages/Creation/Create.tsx b/src/pages/Creation/Create.tsx index e69de29..ba6a7e5 100644 --- a/src/pages/Creation/Create.tsx +++ b/src/pages/Creation/Create.tsx @@ -0,0 +1,39 @@ +import { PenLineIcon } from "lucide-react" + +export default function Create({ mnemonic, onSaved }: { + mnemonic: string + onSaved: () => void +}) { + return ( +
+
+
+ +

+ Backup words +

+
+

+ Write mnemonic into a safe place, may be needed in future. +

+
+
+ {mnemonic.split(' ').map((word, index) => ( + + ))} +
+ +
+ ) +} \ No newline at end of file diff --git a/src/pages/Creation/Import.tsx b/src/pages/Creation/Import.tsx index e69de29..8ff97e5 100644 --- a/src/pages/Creation/Import.tsx +++ b/src/pages/Creation/Import.tsx @@ -0,0 +1,61 @@ +import { useState, useCallback } from "react" +import { SquareAsteriskIcon } from "lucide-react" + +export default function Import ({ onSubmit }: { + onSubmit: (mnemonic: string) => void +}) { + const [ mnemonic, setMnemonic ] = useState(Array(24).fill("")) + + const changeMnemonicWord = useCallback((index: number, word: string) => { + setMnemonic((prevWords) => { + const updatedWords = [ ...prevWords ] + updatedWords[index] = word + + return updatedWords + }) + }, []) + + const parsePastedMnemonic = useCallback((e: React.ClipboardEvent) => { + const words = e.clipboardData.getData('text').split(' ') + + if (words.length === 24) { + e.preventDefault() + setMnemonic(words) + e.currentTarget.blur() + } + }, []) + + return ( +
+
+
+ +

+ Import a wallet +

+
+

+ Enter mnemonic, theq key of a wallet. +

+
+
+ {mnemonic.map((word, index) => ( + e.currentTarget.type = 'text'} + onBlur={(e) => e.currentTarget.type = 'password'} + onChange={(e) => changeMnemonicWord(index, e.target.value)} + onPaste={parsePastedMnemonic} + /> + ))} +
+ +
+ ) +} \ No newline at end of file diff --git a/src/pages/Creation/Landing.tsx b/src/pages/Creation/Landing.tsx index e69de29..b0e087f 100644 --- a/src/pages/Creation/Landing.tsx +++ b/src/pages/Creation/Landing.tsx @@ -0,0 +1,19 @@ +import Icon from "../../../public/favicon.png?url" +import { Tabs } from "../Creation" + +export default function Landing({ forward }: { + forward: (tab: Tabs) => void +}) { + return ( +
+ +

+ Welcome to Kaspian Wallet! +

+
+ + +
+
+ ) +} \ No newline at end of file diff --git a/src/pages/Creation/Password.tsx b/src/pages/Creation/Password.tsx index e69de29..9c26bc9 100644 --- a/src/pages/Creation/Password.tsx +++ b/src/pages/Creation/Password.tsx @@ -0,0 +1,84 @@ +import { KeyIcon } from "lucide-react" +import { useState, useMemo } from "react" + +enum PasswordErrors { + TooShort, + UpperCase, + LowerCase, + Number, + SpecialCharacter +} + +const specialCharacters = new Set([ "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "=", "+", "[", "]", "{", "}", "|", ";", ":", "'", "\"", "<", ">", ",", ".", "?", "/" ]) + +export default function Password ({ onSet }: { + onSet: (password: string) => void +}) { + const [ password, setPassword ] = useState("") + const [ isHidden, setIsHidden ] = useState(true) + + const errors = useMemo(() => { + const errors = new Set() + + if (password.length < 8) errors.add(PasswordErrors.TooShort) + if (!/[A-Z]/.test(password)) errors.add(PasswordErrors.UpperCase) + if (!/[a-z]/.test(password)) errors.add(PasswordErrors.LowerCase) + if (!/[0-9]/.test(password)) errors.add(PasswordErrors.Number) + if (![...password].some((character) => specialCharacters.has(character))) errors.add(PasswordErrors.SpecialCharacter) + + return errors + }, [ password ]) + + return ( +
+
+
+ +

+ Set a password +

+
+

+ Will be used for encryption of mnemonic. +

+
+
+ setPassword(e.target.value)} + /> + +
+ + + + + +
+
+ +
+ ) +} \ No newline at end of file diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..fcbc0e9 --- /dev/null +++ b/src/style.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +#root { + display: flex; + flex-direction: column; + align-items: center; +} + +main { + max-width: 460px; +} diff --git a/tailwind.config.js b/tailwind.config.js index 62a22f4..f6a77a3 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,72 +1,14 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - darkMode: ["class"], - content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"], - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px" - } - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))" - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))" - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))" - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))" - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))" - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))" - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))" - } - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)" - }, - keyframes: { - "accordion-down": { - from: { height: 0 }, - to: { height: "var(--radix-accordion-content-height)" } - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: 0 } - } - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out" - } - } + content: [ + "./index.html", + "./src/**/*.{js,jsx,ts,tsx}" + ], + daisyui: { + themes: [ "light", "dark", "cupcake" ], // TODO: custom themes }, - plugins: [require("tailwindcss-animate"), require('daisyui')] + plugins: [ + require('daisyui') + ] }