diff --git a/ui/account.js b/ui/account.js index 37f5b98..c0be441 100644 --- a/ui/account.js +++ b/ui/account.js @@ -1,7 +1,8 @@ -import Account from './components/account/Account.svelte' +import { createRoot } from "react-dom"; +import React from "react"; -const app = new Account({ - target: document.getElementById('app'), -}) +import App from "./apps/account/App.tsx"; -export default app +const root = createRoot(document.getElementById("app")); + +root.render(React.createElement(App)); diff --git a/ui/apps/account/App.scss b/ui/apps/account/App.scss new file mode 100644 index 0000000..2cfd29a --- /dev/null +++ b/ui/apps/account/App.scss @@ -0,0 +1,702 @@ +@import "../../styles/global"; +@import "../../styles/artwork"; +@import "../../styles/mixins"; +@import "../../styles/vars"; + +main { + position: relative; + transition: opacity 1000ms; + + &.fade-out { + opacity: 0; + pointer-events: none; + } +} + +.art, +.content { + position: absolute; + top: 0; + bottom: 0; + width: 50%; +} + +.art { + left: 0; + background-image: $lume-base64; // embedded base64 instead of remote image to circumvent loading delay, subject to optimization (bg color) + background-size: cover; + background-position: 50%; + border-right: 1px solid #363636; + + > div { + position: absolute; + inset: 0; + transition: opacity 500ms; + } + + .gradient-1 { + background: linear-gradient( + 272.67deg, + #1fc3f7 -27.49%, + #33a653 49.4%, + #62c554 87.63% + ); + z-index: -1; + } + + .gradient-2 { + background: conic-gradient( + from 180deg at 50% 50%, + #33a653 -15.8deg, + #080808 222.32deg, + #33a653 344.2deg, + #080808 582.32deg + ); + opacity: 0; + z-index: -2; + } + + .gradient-3 { + background: linear-gradient( + 272.67deg, + #ed6a5e -27.49%, + #0c0c0d 26.91%, + #33a653 49.4%, + #ed6a5e 99.62% + ); + opacity: 0; + z-index: -3; + } +} + +main.sign-in, +main.create-account { + .art { + .gradient-1 { + opacity: 0; + } + + .gradient-2 { + opacity: 1; + } + } +} + +main.create-account-step-4 { + .art { + .gradient-2 { + opacity: 0; + transition-delay: 4500ms; + } + + .gradient-3 { + opacity: 1; + transition-delay: 4500ms; + } + } +} + +.content { + left: 50%; + display: flex; + flex-direction: column; + align-items: center; + padding: 5.5em 3.75%; + color: #fff; + background: #080808; + overflow: auto; + + > div:first-child { + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + max-width: 45.8em; + + > div { + flex-grow: 1; + } + } + + > div.grant-info { + @include fluid-font-size(1rem); + margin-top: 5em; + max-width: 28.625em; + line-height: 125%; + color: #808080; + transition: opacity 500ms; + + a { + color: #fff; + } + } +} + +h1 { + @include fluid-font-size(3.125rem); + font-family: $font-family-jetbrains-mono; + line-height: 110%; + text-shadow: 0.017em 0.017em 0.034em #000; +} + +p { + @include fluid-font-size(1.25rem); + margin-top: 1.4em; + line-height: 122%; + color: #808080; +} + +#content-text-wrapper { + position: relative; + margin-bottom: 3.2em; + transition: height 500ms, opacity 500ms; + + > div { + top: 0; + left: 0; + right: 0; + transition: opacity 500ms; + } +} + +#content-text-default { + z-index: 2; +} + +#content-text-create-account, +#content-text-show-key { + position: absolute; + opacity: 0; + z-index: 1; +} + +#switch { + position: relative; + transition: height 500ms; + + > div { + top: 0; + left: 0; + right: 0; + transition: opacity 500ms; + } +} + +#switch-show-key { + position: absolute; + opacity: 0; + z-index: -1; +} + +.warning { + display: flex; + align-items: center; + gap: 2em; + color: #e15858; + + svg { + flex-shrink: 0; + @include fluid-width-height(3.5rem, 3.5rem); + margin-bottom: -0.5em; + } + + div { + @include fluid-font-size(1.25rem); + line-height: 122%; + } +} + +.content-action-wrapper { + position: relative; + height: 6.2em; + transition: height 500ms, margin-bottom 500ms; + transition-timing-function: cubic-bezier(0.68, -0.6, 0.32, 1.6); +} + +.content-action-sign-in { + position: absolute; + inset: 0; + transition: opacity 500ms; + + .content-action-inner { + position: relative; + + .sign-in-btn { + transition: opacity 250ms; + } + + .sign-in-form { + position: absolute; + top: 0; + left: 0; + right: 0; + opacity: 0; + transition: opacity 250ms 250ms; + z-index: -1; + + .input-wrapper { + display: flex; + height: 6.2em; + + input { + box-sizing: content-box; + flex: 1; + display: inline-block; + @include fluid-font-size(1.25rem); + height: 1em; + padding: 1em; + background: #080808; + border: 0.05em solid #777; + border-radius: 0.25em; + text-align: center; + transition: border-color 250ms; + + &::placeholder { + color: #808080; + } + + &:hover { + border-color: #a1a1a1; + } + + &:focus { + outline: none; + border-color: #fff; + } + } + } + + .btn-wrapper { + margin-top: 3em; + } + } + } +} + +.content-action-create-account { + position: absolute; + inset: 0; + transition: opacity 500ms; + opacity: 0; + z-index: -1; + + .content-action-inner { + position: relative; + + .create-account-ready-btn { + transition: opacity 250ms; + } + + .create-account-ready { + position: absolute; + top: 0; + left: 0; + right: 0; + opacity: 0; + transition: opacity 250ms; + z-index: -1; + } + + .warning { + height: 5.6em; + margin-top: 2em; + } + + .create-account-show-key-btn { + margin-top: 4em; + + button { + position: relative; + padding-right: 4.1em; + overflow: hidden; + + &::after { + content: ""; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 3.1em; + color: #fff; + background-color: #62c554; + background-image: url('data:image/svg+xml,'); + background-size: 1.125em 1.125em; + background-repeat: no-repeat; + background-position: 50%; + transition: background-color 500ms; + } + + &:hover::after { + background-color: #6ee65d; + } + } + } + + .create-account-back-btn { + margin-top: 2em; + + button { + @include fluid-font-size(1.125rem); + line-height: 1; + color: #fff; + padding: 1.25em; + + span { + background: #080808; + padding: 0 0.75em; + } + } + } + } +} + +.separator { + position: relative; + height: 1px; + background: #62c554; + margin: 4.5em 0; + transition: background 500ms, opacity 500ms; + + span { + @include fluid-font-size(1.25rem); + display: inline-block; + position: absolute; + left: 50%; + top: 50%; + padding: 0 0.75em; + color: #62c554; + background: #080808; + transform: translate(-50%, -50%); + transition: color 500ms; + } +} + +.btn-stack { + display: grid; + transition: opacity 500ms; + + > div { + grid-column-start: 1; + grid-row-start: 1; + transition: opacity 500ms; + } +} + +.create-account-btn { + opacity: 1; + z-index: 2; +} + +.create-account-gray-btn, +.create-account-cancel-btn { + opacity: 0; + z-index: 1; +} + +.show-key-wrapper { + .warning { + height: 0; + margin: 1.6em 0; + opacity: 0; + transition: height 500ms, margin 500ms, opacity 500ms; + } + + .show-key-copy-btn button { + display: flex; + align-items: center; + justify-content: center; + + svg { + @include fluid-width-height(1.75rem, 1.75rem); + margin: -50% 0.625em -50% 0; + color: #d9d9d9; + transition: color $transition-duration; + } + + &:hover { + svg { + color: inherit; + } + } + + &.success { + color: #62c554; + border-color: #62c554; + + svg { + color: #62c554; + } + } + + &.error { + color: #f66060; + border-color: #f66060; + + svg { + color: #f66060; + } + } + } + + .btn-stack { + margin-top: 1.8em; + } +} + +.show-key-continue-btn { + z-index: 2; +} + +.show-key-login-btn { + z-index: 1; + opacity: 0; +} + +.show-key-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.6em; + + > div { + position: relative; + @include fluid-font-size(1.25rem); + line-height: 1; + padding: 0.8em 0.8em 0.8em 1.6em; + border: 0.05em solid #444; + border-radius: 0.25em; + + span { + position: absolute; + @include fluid-font-size(1rem); + top: 0.5em; + left: 0.5em; + color: #969696; + } + } +} + +main.sign-in { + .content-action-wrapper { + height: 15.4em; + } + + .content-action-sign-in { + .sign-in-btn { + opacity: 0; + } + + .sign-in-form { + z-index: 1; + opacity: 1; + } + } + + .separator { + background: #777; + + span { + color: #777; + } + } + + .create-account-btn { + opacity: 0; + z-index: 1; + } + + .create-account-gray-btn { + opacity: 1; + z-index: 2; + } +} + +main.create-account { + #content-text-default { + opacity: 0; + z-index: 1; + } + + #content-text-create-account { + opacity: 1; + z-index: 2; + } + + .content-action-sign-in { + opacity: 0; + z-index: 1; + } + + .content-action-create-account { + opacity: 1; + z-index: 2; + } + + .separator { + background: #777; + + span { + color: #777; + } + } + + .create-account-btn { + opacity: 0; + z-index: 1; + } + + .create-account-cancel-btn { + opacity: 1; + z-index: 2; + } + + &.create-account-step-2, + &.create-account-step-3 { + #content-text-wrapper, + .separator, + #switch-default .btn-stack, + .grant-info { + transition-delay: 2s; + opacity: 0.15; + pointer-events: none; + } + + .content-action-wrapper { + animation: 2500ms create-account-warning; + height: 25.8em; + margin-bottom: -7.6em; + } + + .content-action-create-account { + overflow: hidden; + } + + .create-account-ready-btn { + opacity: 0; + z-index: 1; + } + + .create-account-ready { + opacity: 1; + z-index: 2; + } + } + + &.create-account-step-3, + &.create-account-step-4 { + #content-text-wrapper, + .separator, + #switch-default .btn-stack, + .grant-info { + transition-delay: 0s; + opacity: 1; + } + + .grant-info { + pointer-events: auto; + } + + #content-text-default { + opacity: 0; + z-index: 1; + } + + #content-text-create-account { + opacity: 0; + z-index: 1; + } + + #content-text-show-key { + opacity: 1; + z-index: 2; + } + + #switch-default { + opacity: 0; + z-index: 1; + pointer-events: none; + } + + #switch-show-key { + opacity: 1; + z-index: 2; + } + } + + &.create-account-step-4 { + #content-text-wrapper, + .show-key-copy-btn, + #switch-show-key .btn-stack, + .grant-info { + animation: 5000ms save-key-warning; + } + + .show-key-wrapper .warning { + height: 5.6em; + margin: 3.2em 0; + opacity: 1; + } + + .show-key-continue-btn { + z-index: 1; + opacity: 0; + transition-delay: 4500ms; + } + + .show-key-login-btn { + z-index: 2; + opacity: 1; + transition-delay: 4500ms; + } + } +} + +@media screen and (max-width: 48rem) { + .art { + display: none; + } + + .content { + left: 0; + width: 100%; + } +} + +@keyframes create-account-warning { + 0% { + height: 6.2em; + margin-bottom: 0; + } + + 20% { + height: 9.6em; + } + + 80% { + height: 9.6em; + margin-bottom: 0; + } + + 100% { + height: 25.8em; + margin-bottom: -7.6em; + } +} + +@keyframes save-key-warning { + 0% { + opacity: 1; + } + + 10% { + opacity: 0.15; + } + + 90% { + opacity: 0.15; + } + + 100% { + opacity: 1; + } +} diff --git a/ui/apps/account/App.tsx b/ui/apps/account/App.tsx new file mode 100644 index 0000000..0016978 --- /dev/null +++ b/ui/apps/account/App.tsx @@ -0,0 +1,438 @@ +// @ts-ignore +import lumeLogo from "../../assets/lume-logo.png"; +// @ts-ignore +import classNames from "classnames"; +import { useEffect, useRef, useState } from "react"; +import * as bip39 from "@scure/bip39"; +import { wordlist } from "@scure/bip39/wordlists/english"; +import { HDKey } from "ed25519-keygen/hdkey"; +import { x25519 } from "@noble/curves/ed25519"; +// @ts-ignore +import browser from "webextension-polyfill"; +import { bytesToHex, hexToBytes, randomBytes } from "@lumeweb/libweb"; +import { secretbox } from "@noble/ciphers/salsa"; +import "./App.scss"; + +const BIP44_PATH = "m/44'/1627'/0'/0'/0'"; + +export default function App() { + let [currentAction, setCurrentAction] = useState< + "sign-in" | "create-account" | null + >(null); + let [createAccountStep, setCreateAccountStep] = useState(0); + let [generatedKey, setGeneratedKey] = useState([]); + let [copyKeySuccess, setCopyKeySuccess] = useState(false); + let [copyKeyError, setCopyKeyError] = useState(false); + + let elSwitch = useRef(); + let elSwitchDefault = useRef(); + let elSwitchShowKey = useRef(); + let elContentTextActive = useRef(); + let elContentTextDefault = useRef(); + let elContentTextCreateAccount = useRef(); + let elContentTextWrapper = useRef(); + let elContentTextShowKey = useRef(); + let resizeTimeout = useRef(); + let copyKeyTimeout = useRef(); + let copyKeyCallback = useRef(); + let elInputKey = useRef(); + + function createAccount() { + setCurrentAction("create-account"); + setCreateAccountStep(1); + } + + function createAccountCancel() { + setCurrentAction(null); + setCreateAccountStep(0); + } + + function signIn() { + setCurrentAction("sign-in"); + } + + function inputKeySignIn() { + if (!bip39.validateMnemonic(elInputKey.current.value, wordlist)) { + alert("invalid key"); + return; + } + + processSignIn(elInputKey.current.value); + } + + function createAccountReady() { + if (createAccountStep !== 1) { + return; + } + + setCreateAccountStep(2); + } + + function showKey() { + if (createAccountStep !== 2) { + return; + } + + // generate key + setGeneratedKey(bip39.generateMnemonic(wordlist).split(" ")); + + setCreateAccountStep(3); + elSwitch.current.style.height = elSwitchDefault.current.offsetHeight + "px"; + elSwitchDefault.current.style.position = "absolute"; + + setTimeout(() => { + setContentTextHeight(elContentTextShowKey); + elSwitch.current.style.height = + elSwitchShowKey.current.offsetHeight + "px"; + + elSwitch.current.addEventListener("transitionend", (event) => { + if (event.target !== elSwitch.current) { + return; + } + + elSwitchShowKey.current.style.position = "static"; + elSwitch.current.style.height = ""; + }); + }, 0); + } + + function createAccountBack() { + setCreateAccountStep(1); + } + + function showKeyWarning() { + if (createAccountStep !== 3) { + return; + } + + setCreateAccountStep(4); + } + + function generatedKeySignIn() { + const seed = generatedKey.join(" "); + + if (!bip39.validateMnemonic(seed, wordlist)) { + alert("invalid key"); + return; + } + + processSignIn(seed); + } + + const processSignIn = async (wordSeed) => { + const seed = await bip39.mnemonicToSeed(wordSeed); + const key = HDKey.fromMasterSeed(seed).derive(BIP44_PATH); + + let pubKey; + let privKey = x25519.utils.randomPrivateKey(); + + try { + pubKey = await browser.runtime.sendMessage({ + method: "exchangeCommunicationKeys", + data: bytesToHex(x25519.getPublicKey(privKey)), + }); + } catch (e) { + alert(`Failed to login: ${e.message}`); + return; + } + + if (!pubKey) { + alert(`Failed to login: could not get communication key`); + return; + } + pubKey = hexToBytes(pubKey); + + const secret = x25519.getSharedSecret(privKey, pubKey); + const nonce = randomBytes(24); + const box = secretbox(secret, nonce); + const ciphertext = box.seal(key.privateKey); + + await browser.runtime.sendMessage({ + method: "setLoginKey", + data: { + data: bytesToHex(ciphertext), + nonce: bytesToHex(nonce), + }, + }); + + window.setTimeout(() => { + window.location.href = "/dashboard.html"; + }, 1000); + }; + + function setContentTextHeight(element) { + elContentTextActive.current = element; + elContentTextWrapper.current.style.height = element.offsetHeight + "px"; + } + + function copyKey() { + clearTimeout(copyKeyTimeout.current); + + navigator.clipboard + .writeText(generatedKey.join(" ")) + .then(() => { + copyKeyCallback.current?.(); + + setCopyKeySuccess(true); + + copyKeyCallback.current = () => { + copyKeyCallback.current = undefined; + setCopyKeySuccess(null); + }; + + copyKeyTimeout.current = setTimeout(copyKeyCallback.current, 750); + }) + .catch((error) => { + console.error(error); + + copyKeyCallback.current?.(); + + setCopyKeyError(true); + + copyKeyCallback.current = () => { + copyKeyCallback.current = undefined; + setCopyKeyError(false); + }; + + copyKeyTimeout.current = setTimeout(copyKeyCallback.current, 750); + }); + } + + useEffect(() => { + const onResize = () => { + clearTimeout(resizeTimeout.current); + + resizeTimeout.current = setTimeout(() => { + setContentTextHeight(elContentTextActive); + }, 25); + }; + + window.addEventListener("resize", onResize); + }); + + return ( + <> +
+ Lume +
+
+
+
+
+
+
+
+
+
+
+
+

Access the open web with ease

+

+ Seamless access to the open web with Lume, integrated + Handshake (HNS) and Ethereum (ENS) Support. +

+
+
+

Set up your account key

+

+ Let’s create your account key or seed phrase. This phrase + will be used to access your Lume account. Make sure to keep + it safe at all times. +

+
+
+

Here’s your account key

+

Make sure to keep it safe at all times.

+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+
+ + + +
+ Should you lose this key, you will be forced to + create a new account. +
+
+
+ +
+
+ +
+
+
+
+
+
+ OR +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ {createAccountStep > 2 && ( +
+
+ {generatedKey.map((word, index) => ( +
+ {index + 1} + {word} +
+ ))} +
+
+ + + +
+ Make sure to write this key down for safe keeping. +
+
+
+ +
+
+
+ +
+
+ +
+
+
+ )} +
+
+
+
+
+
+ + ); +}