wip: new import account

This commit is contained in:
reya 2023-10-15 16:10:16 +07:00
parent 620e763380
commit cd3b9ada5a
13 changed files with 430 additions and 418 deletions

View File

@ -20,8 +20,8 @@
"dependencies": {
"@evilmartians/harmony": "^1.1.0",
"@getalby/sdk": "^2.4.0",
"@nostr-dev-kit/ndk": "^2.0.1",
"@nostr-dev-kit/ndk-cache-dexie": "^2.0.1",
"@nostr-dev-kit/ndk": "^2.0.2",
"@nostr-dev-kit/ndk-cache-dexie": "^2.0.2",
"@nostr-fetch/adapter-ndk": "^0.12.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
@ -56,6 +56,7 @@
"@tiptap/suggestion": "^2.1.12",
"dayjs": "^1.11.10",
"destr": "^2.0.1",
"framer-motion": "^10.16.4",
"html-to-text": "^9.0.5",
"light-bolt11-decoder": "^3.0.0",
"lru-cache": "^10.0.1",
@ -75,6 +76,7 @@
"react-string-replace": "^1.1.1",
"reactflow": "^11.9.3",
"remark-gfm": "^3.0.1",
"sonner": "^1.0.3",
"tailwind-scrollbar": "^3.0.5",
"tauri-controls": "^0.2.0",
"tippy.js": "^6.3.7",

View File

@ -12,14 +12,14 @@ dependencies:
specifier: ^2.4.0
version: 2.4.0
'@nostr-dev-kit/ndk':
specifier: ^2.0.1
version: 2.0.1(typescript@5.2.2)
specifier: ^2.0.2
version: 2.0.2(typescript@5.2.2)
'@nostr-dev-kit/ndk-cache-dexie':
specifier: ^2.0.1
version: 2.0.1(typescript@5.2.2)
specifier: ^2.0.2
version: 2.0.2(typescript@5.2.2)
'@nostr-fetch/adapter-ndk':
specifier: ^0.12.2
version: 0.12.2(@nostr-dev-kit/ndk@2.0.1)(nostr-fetch@0.13.0)
version: 0.12.2(@nostr-dev-kit/ndk@2.0.2)(nostr-fetch@0.13.0)
'@radix-ui/react-alert-dialog':
specifier: ^1.0.5
version: 1.0.5(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0)
@ -119,6 +119,9 @@ dependencies:
destr:
specifier: ^2.0.1
version: 2.0.1
framer-motion:
specifier: ^10.16.4
version: 10.16.4(react-dom@18.2.0)(react@18.2.0)
html-to-text:
specifier: ^9.0.5
version: 9.0.5
@ -176,6 +179,9 @@ dependencies:
remark-gfm:
specifier: ^3.0.1
version: 3.0.1
sonner:
specifier: ^1.0.3
version: 1.0.3(react-dom@18.2.0)(react@18.2.0)
tailwind-scrollbar:
specifier: ^3.0.5
version: 3.0.5(tailwindcss@3.3.3)
@ -551,6 +557,20 @@ packages:
'@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0
/@emotion/is-prop-valid@0.8.8:
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
requiresBuild: true
dependencies:
'@emotion/memoize': 0.7.4
dev: false
optional: true
/@emotion/memoize@0.7.4:
resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
requiresBuild: true
dev: false
optional: true
/@esbuild/android-arm64@0.18.20:
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'}
@ -920,10 +940,10 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
/@nostr-dev-kit/ndk-cache-dexie@2.0.1(typescript@5.2.2):
resolution: {integrity: sha512-O1ngV95yuZPhV0PB6JQAMHQkZvGtcW6qEY1jawvrZCfYLf2vdHWuzMN2rXYiSdrx6mMsnqB17bq5Lg3r8Coslw==}
/@nostr-dev-kit/ndk-cache-dexie@2.0.2(typescript@5.2.2):
resolution: {integrity: sha512-v6dq82Gzw/AoDMtkjCeTg+gx9n6sX3xReaMpIUbKL5W+E7z1lqnR0RmYkeNxUhd7Tlg0FQ+Ywqq7nZs+UmGDEA==}
dependencies:
'@nostr-dev-kit/ndk': 2.0.1(typescript@5.2.2)
'@nostr-dev-kit/ndk': 2.0.2(typescript@5.2.2)
debug: 4.3.4
dexie: 3.2.4
nostr-tools: 1.16.0(typescript@5.2.2)
@ -933,8 +953,8 @@ packages:
- typescript
dev: false
/@nostr-dev-kit/ndk@2.0.1(typescript@5.2.2):
resolution: {integrity: sha512-LZ7h4HL2B0Yek3Pr276OMaiVzr6WYXSWExZKn8bdpZ5lIzt5t1j4bi8kxwfUZti1Z/nIY7Hq7tIguty39YBs/g==}
/@nostr-dev-kit/ndk@2.0.2(typescript@5.2.2):
resolution: {integrity: sha512-EwaOJVS0FOCXlIffiVceKrK+QtbaRTG6QYdoQchMAe+ag2C3jl7nAoDTWlixv/WgJOFl4KPQkS8r0sEkGmXsjQ==}
dependencies:
'@noble/hashes': 1.3.2
'@noble/secp256k1': 2.0.0
@ -952,13 +972,13 @@ packages:
- typescript
dev: false
/@nostr-fetch/adapter-ndk@0.12.2(@nostr-dev-kit/ndk@2.0.1)(nostr-fetch@0.13.0):
/@nostr-fetch/adapter-ndk@0.12.2(@nostr-dev-kit/ndk@2.0.2)(nostr-fetch@0.13.0):
resolution: {integrity: sha512-+7EVuxS5DDZvNo6qbfFp7xRHwIyjyi36hYkiQFDjbQ4gX5LKo9RIPB1P+1XGkOSDFshypTbovZCaFunscJ/zhQ==}
peerDependencies:
'@nostr-dev-kit/ndk': ^0.7.5
nostr-fetch: ^0.12.2
dependencies:
'@nostr-dev-kit/ndk': 2.0.1(typescript@5.2.2)
'@nostr-dev-kit/ndk': 2.0.2(typescript@5.2.2)
'@nostr-fetch/kernel': 0.12.2
nostr-fetch: 0.13.0
dev: false
@ -3082,7 +3102,7 @@ packages:
postcss: ^8.1.0
dependencies:
browserslist: 4.22.1
caniuse-lite: 1.0.30001547
caniuse-lite: 1.0.30001549
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.0.0
@ -3134,7 +3154,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001547
caniuse-lite: 1.0.30001549
electron-to-chromium: 1.4.554
node-releases: 2.0.13
update-browserslist-db: 1.0.13(browserslist@4.22.1)
@ -3163,8 +3183,8 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
/caniuse-lite@1.0.30001547:
resolution: {integrity: sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==}
/caniuse-lite@1.0.30001549:
resolution: {integrity: sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA==}
/case-anything@2.1.13:
resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==}
@ -3997,6 +4017,24 @@ packages:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: true
/framer-motion@10.16.4(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-p9V9nGomS3m6/CALXqv6nFGMuFOxbWsmaOrdmhyQimMIlLl3LC7h7l86wge/Js/8cRu5ktutS/zlzgR7eBOtFA==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.6.2
optionalDependencies:
'@emotion/is-prop-valid': 0.8.8
dev: false
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@ -5464,7 +5502,7 @@ packages:
dependencies:
lilconfig: 2.1.0
postcss: 8.4.31
yaml: 2.3.2
yaml: 2.3.3
/postcss-nested@6.0.1(postcss@8.4.31):
resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
@ -5801,7 +5839,7 @@ packages:
remark-parse: 10.0.2
remark-rehype: 10.1.0
space-separated-tokens: 2.0.2
style-to-object: 0.4.2
style-to-object: 0.4.3
unified: 10.1.2
unist-util-visit: 4.1.2
vfile: 5.3.7
@ -6140,6 +6178,16 @@ packages:
is-fullwidth-code-point: 4.0.0
dev: true
/sonner@1.0.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-hBoA2zKuYW3lUnpx4K0vAn8j77YuYiwvP9sLQfieNS2pd5FkT20sMyPTDJnl9S+5T27ZJbwQRPiujwvDBwhZQg==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
@ -6230,8 +6278,8 @@ packages:
engines: {node: '>=8'}
dev: true
/style-to-object@0.4.2:
resolution: {integrity: sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA==}
/style-to-object@0.4.3:
resolution: {integrity: sha512-RP9icVx0g3Pt0CyNiC2qvBkqMTHD5uBVC2XYcSr/ag8QWKApx/oXEh2ehMGSyzkjK0+ySkukMuO+mz+DNQq57Q==}
dependencies:
inline-style-parser: 0.1.1
dev: false
@ -6850,8 +6898,8 @@ packages:
engines: {node: '>= 14'}
dev: true
/yaml@2.3.2:
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
/yaml@2.3.3:
resolution: {integrity: sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ==}
engines: {node: '>= 14'}
/yocto-queue@0.1.0:

30
src-tauri/Cargo.lock generated
View File

@ -1087,6 +1087,7 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
dependencies = [
"powerfmt",
"serde",
]
@ -3445,6 +3446,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -3681,14 +3688,14 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.0"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87"
checksum = "aaac441002f822bc9705a681810a4dd2963094b9ca0ddc41cb963a4c189189ea"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.1",
"regex-syntax 0.8.1",
"regex-automata 0.4.2",
"regex-syntax 0.8.2",
]
[[package]]
@ -3702,13 +3709,13 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b"
checksum = "5011c7e263a695dc8ca064cddb722af1be54e517a280b12a5356f98366899e5d"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.1",
"regex-syntax 0.8.2",
]
[[package]]
@ -3719,9 +3726,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "reqwest"
@ -5322,12 +5329,13 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.29"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe"
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
dependencies = [
"deranged",
"itoa 1.0.9",
"powerfmt",
"serde",
"time-core",
"time-macros",

View File

@ -3,8 +3,7 @@ import { fetch } from '@tauri-apps/plugin-http';
import { RouterProvider, createBrowserRouter, defer, redirect } from 'react-router-dom';
import { ReactFlowProvider } from 'reactflow';
import { AuthCreateScreen } from '@app/auth/create';
import { AuthImportScreen } from '@app/auth/import';
import { CreateAccountScreen } from '@app/auth/create';
import { OnboardingScreen } from '@app/auth/onboarding';
import { ChatsScreen } from '@app/chats';
import { ErrorScreen } from '@app/error';
@ -28,7 +27,7 @@ export default function App() {
const totalAccount = await db.checkAccount();
const onboarding = localStorage.getItem('onboarding');
const step = JSON.parse(onboarding).state.step || null;
const step = onboarding ? JSON.parse(onboarding).state.step : null;
// redirect to welcome screen if none user exist
if (totalAccount === 0) return redirect('/auth/welcome');
@ -168,48 +167,18 @@ export default function App() {
return { Component: WelcomeScreen };
},
},
{
path: 'import',
element: <AuthImportScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { ImportStep1Screen } = await import('@app/auth/import/step-1');
return { Component: ImportStep1Screen };
},
},
{
path: 'step-2',
async lazy() {
const { ImportStep2Screen } = await import('@app/auth/import/step-2');
return { Component: ImportStep2Screen };
},
},
],
},
{
path: 'create',
element: <AuthCreateScreen />,
element: <CreateAccountScreen />,
errorElement: <ErrorScreen />,
children: [
{
path: '',
async lazy() {
const { CreateStep1Screen } = await import('@app/auth/create/step-1');
return { Component: CreateStep1Screen };
},
},
{
path: 'step-2',
path: 'import',
async lazy() {
const { CreateStep2Screen } = await import('@app/auth/create/step-2');
return { Component: CreateStep2Screen };
const { ImportAccountScreen } = await import('@app/auth/import');
return { Component: ImportAccountScreen };
},
},
],
},
{
path: 'onboarding',
element: <OnboardingScreen />,

View File

@ -1,9 +1,5 @@
import { Outlet } from 'react-router-dom';
export function AuthCreateScreen() {
return (
<div className="flex h-full w-full items-center justify-center">
<Outlet />
</div>
);
export function CreateAccountScreen() {
return <Outlet />;
}

View File

@ -16,7 +16,6 @@ export function CreateStep1Screen() {
const { db } = useStorage();
const navigate = useNavigate();
const setTempPrivkey = useOnboarding((state) => state.setTempPrivkey);
const setPubkey = useOnboarding((state) => state.setPubkey);
const setStep = useOnboarding((state) => state.setStep);
@ -29,6 +28,17 @@ export function CreateStep1Screen() {
const npub = nip19.npubEncode(pubkey);
const nsec = nip19.nsecEncode(privkey);
const copyPrivkey = async () => {
try {
await writeText(nsec);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
await message(e, { title: 'Cannot copy private key', type: 'error' });
}
};
const download = async () => {
try {
const downloadPath = await downloadDir();
@ -50,29 +60,22 @@ export function CreateStep1Screen() {
}
};
const copyPrivkey = async () => {
try {
await writeText(nsec);
setCopied(true);
setTimeout(() => setCopied(false), 3000);
} catch (e) {
await message(e, { title: 'Cannot copy private key', type: 'error' });
}
};
const submit = async () => {
try {
setLoading(true);
// update state
setTempPrivkey(privkey); // only use if user close app and reopen it
setPubkey(pubkey);
// save privkey
await db.secureSave(privkey, pubkey);
// save to database
await db.createAccount(npub, pubkey);
// redirect to next step
navigate('/auth/create/step-2', { replace: true });
} catch (e) {
await message(e, { title: 'Something went wrong!', type: 'error' });
}
};
useEffect(() => {
@ -81,71 +84,87 @@ export function CreateStep1Screen() {
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-4 border-b border-white/10 pb-4">
<h1 className="mb-2 text-center text-2xl font-semibold text-white">
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
<div>
<h1 className="mb-2 text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
This is your new Nostr account
</h1>
<p className="mb-2 text-white/70">
Your private key is your password. If you lose this key, you will lose access to
your account! Copy it and keep it in a safe place. There is no way to reset your
private key.
<p className="mb-2 select-text text-neutral-600 dark:text-neutral-300">
Your private key is your password. If you lose this key, you will lose access
to your account! Copy it and keep it in a safe place.{' '}
<span className="text-red-500">
There is no way to reset your private key.
</span>
</p>
<p className="text-white/70">
Public key is used for sharing with other people so that they can find you using
the public key.
<p className="select-text text-neutral-600 dark:text-neutral-300">
Public key is used for sharing with other people so that they can find you
using the public key.
</p>
</div>
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<span className="font-medium text-white">Private Key</span>
<label
htmlFor="nsec"
className="font-medium text-neutral-900 dark:text-neutral-100"
>
Private Key
</label>
<div className="relative">
<input
readOnly
name="nsec"
value={nsec.substring(0, 5) + '**************************************'}
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 py-1 pl-3.5 pr-11 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
className="relative h-12 w-full rounded-lg bg-neutral-200 py-1 pl-3.5 pr-11 text-neutral-900 !outline-none dark:bg-neutral-800 dark:text-neutral-100"
/>
<button
type="button"
onClick={() => copyPrivkey()}
className="group absolute right-2 top-1/2 inline-flex h-7 -translate-y-1/2 transform items-center gap-1.5 rounded-md bg-white/20 px-2.5 text-sm hover:bg-white/30"
className="group absolute right-2 top-1/2 inline-flex h-7 -translate-y-1/2 transform items-center gap-1.5 rounded-md bg-neutral-300 px-2.5 text-sm text-neutral-800 hover:bg-neutral-400 hover:text-neutral-900 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-600 dark:hover:text-neutral-100"
>
<CopyIcon className="h-4 w-4 text-white/70 group-hover:text-white" />
<CopyIcon className="h-4 w-4" />
{copied ? 'Copied' : 'Copy'}
</button>
</div>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium text-white">Public Key</span>
<label
htmlFor="npub"
className="font-medium text-neutral-900 dark:text-neutral-100"
>
Public Key
</label>
<input
readOnly
name="npub"
value={npub}
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
className="relative h-12 w-full rounded-lg bg-neutral-200 px-3.5 py-1 text-neutral-900 !outline-none dark:bg-neutral-800 dark:text-neutral-100"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
<button
type="button"
onClick={() => download()}
className="inline-flex h-12 w-full items-center justify-center rounded-lg bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none"
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none"
>
{downloaded ? 'Downloaded' : 'Download account keys'}
</button>
<button
type="button"
onClick={() => submit()}
className="inline-flex h-12 w-full items-center justify-center rounded-lg border-t border-white/10 bg-white/20 px-6 font-medium leading-none text-white hover:bg-white/30 focus:outline-none"
className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-neutral-200 px-6 font-medium leading-none text-neutral-900 hover:bg-neutral-300 focus:outline-none dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700"
>
{loading ? 'Creating...' : 'Continue'}
</button>
<span className="text-center text-sm text-white/50">
By clicking &apos;Continue&apos;, you are ensuring that your keys are saved in
a safe place. You cannot recover these keys if they are lost.
<span className="select-text text-center text-sm text-neutral-400 dark:text-neutral-600">
By clicking &apos;Continue&apos;, you are ensuring that your keys are saved
in a safe place. You cannot recover these keys if they are lost.
</span>
</div>
</div>
</div>
</div>
);
}

225
src/app/auth/import.tsx Normal file
View File

@ -0,0 +1,225 @@
import { motion } from 'framer-motion';
import { nip19 } from 'nostr-tools';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { twMerge } from 'tailwind-merge';
import { useStorage } from '@libs/storage/provider';
import { User } from '@shared/user';
export function ImportAccountScreen() {
const { db } = useStorage();
const navigate = useNavigate();
const [npub, setNpub] = useState<string>('');
const [nsec, setNsec] = useState<string>('');
const [pubkey, setPubkey] = useState<undefined | string>(undefined);
const [created, setCreated] = useState(false);
const [savedPrivkey, setSavedPrivkey] = useState(false);
const submitNpub = async () => {
if (npub.length < 6) return toast('You must enter valid npub');
if (!npub.startsWith('npub1')) return toast('npub must be starts with npub1');
try {
const pubkey = nip19.decode(npub).data as string;
setPubkey(pubkey);
} catch (e) {
return toast(`npub invalid: ${e}`);
}
};
const changeAccount = async () => {
setNpub('');
setPubkey('');
};
const createAccount = async () => {
try {
await db.createAccount(npub, pubkey);
setCreated(true);
} catch (e) {
return toast(`Create account failed: ${e}`);
}
};
const submitNsec = async () => {
if (savedPrivkey) return;
if (nsec.length > 50 && nsec.startsWith('nsec1')) {
try {
const privkey = nip19.decode(nsec).data as string;
await db.secureSave(pubkey, privkey);
setSavedPrivkey(true);
} catch (e) {
return toast(`nsec invalid: ${e}`);
}
}
};
const finish = async () => {
navigate('/auth/onboarding');
};
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-10">
<h1 className="text-center text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
Import your Nostr account
</h1>
<div className="flex flex-col gap-3">
<div className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200">
<div className="flex flex-col gap-1.5">
<label htmlFor="npub" className="font-semibold">
Enter your nostr npub:
</label>
<div className="inline-flex w-full items-center gap-2">
<input
type="text"
value={npub}
onChange={(e) => setNpub(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="npub1"
className="h-11 flex-1 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
/>
{!pubkey ? (
<button
type="button"
onClick={submitNpub}
className="h-11 w-24 shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
>
Continue
</button>
) : null}
</div>
</div>
</div>
{pubkey ? (
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{
opacity: 1,
y: 0,
}}
transition={{ y: { velocity: -100 } }}
className="rounded-xl bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
>
<h5 className="mb-1.5 font-semibold">Account found</h5>
<div className="flex w-full flex-col gap-2">
<div className="inline-flex h-full flex-1 items-center rounded-lg bg-neutral-200 p-2">
<User pubkey={pubkey} variant="simple" />
</div>
{!created ? (
<div className="flex gap-2">
<button
type="button"
onClick={changeAccount}
className="h-9 flex-1 shrink-0 rounded-lg bg-neutral-200 font-semibold text-neutral-800 hover:bg-neutral-300 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700"
>
Change account
</button>
<button
type="button"
onClick={createAccount}
className="h-9 flex-1 shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
>
Continue
</button>
</div>
) : null}
</div>
</motion.div>
) : null}
{created ? (
<>
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{
opacity: 1,
y: 0,
}}
transition={{ y: { velocity: -100 } }}
className="rounded-lg bg-neutral-100 p-3 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"
>
<div className="flex flex-col gap-1.5">
<label htmlFor="npub" className="font-semibold">
Enter your nostr nsec (optional):
</label>
<div className="inline-flex w-full items-center gap-2">
<input
type="text"
value={nsec}
onChange={(e) => setNsec(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="nsec1"
className="h-11 flex-1 rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400"
/>
<button
type="button"
onClick={submitNsec}
className={twMerge(
'h-11 w-24 shrink-0 rounded-lg font-semibold text-white',
!savedPrivkey
? 'bg-blue-500 hover:bg-blue-600'
: 'bg-teal-500 hover:bg-teal-600'
)}
>
{savedPrivkey ? 'Saved' : 'Save'}
</button>
</div>
</div>
<div className="mt-3 select-text">
<p className="text-sm">
<b>nsec</b> is used to sign your event. For example, if you want to
make a new post or send a message to your contact, you need to use
nsec to sign this event.
</p>
<h5 className="mt-2 font-semibold">
1. In case you store nsec in Lume
</h5>
<p className="text-sm">
Lume will put your nsec to{' '}
{db.platform === 'macos'
? 'Apple Keychain (macOS)'
: db.platform === 'windows'
? 'Credential Manager (Windows)'
: 'Secret Service (Linux)'}
, it will be secured by your OS
</p>
<h5 className="mt-2 font-semibold">
2. In case you do not store nsec in Lume
</h5>
<p className="text-sm">
When you make an event that requires a sign by your nsec, Lume will
show a prompt popup for you to enter nsec. It will be cleared after
signing and not stored anywhere.
</p>
</div>
</motion.div>
<motion.button
type="button"
onClick={finish}
initial={{ opacity: 0, y: 80 }}
animate={{
opacity: 1,
y: 0,
}}
transition={{ y: { velocity: -130 } }}
className="h-9 w-full shrink-0 rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
>
Finish
</motion.button>
</>
) : null}
</div>
</div>
</div>
);
}

View File

@ -1,9 +0,0 @@
import { Outlet } from 'react-router-dom';
export function AuthImportScreen() {
return (
<div className="flex h-full w-full items-center justify-center">
<Outlet />
</div>
);
}

View File

@ -1,156 +0,0 @@
import { getPublicKey, nip19 } from 'nostr-tools';
import { useEffect, useState } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { useOnboarding } from '@stores/onboarding';
type FormValues = {
privkey: string;
};
const resolver: Resolver<FormValues> = async (values) => {
return {
values: values.privkey ? values : {},
errors: !values.privkey
? {
privkey: {
type: 'required',
message: 'This is required.',
},
}
: {},
};
};
export function ImportStep1Screen() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [passwordInput, setPasswordInput] = useState('password');
const [setStep, setPubkey, setTempPrivkey] = useOnboarding((state) => [
state.setStep,
state.setPubkey,
state.setTempPrivkey,
]);
const { db } = useStorage();
const {
register,
setError,
handleSubmit,
formState: { errors, isDirty, isValid },
} = useForm<FormValues>({ resolver });
const onSubmit = async (data: { [x: string]: string }) => {
try {
setLoading(true);
let privkey = data['privkey'];
if (privkey.substring(0, 4) === 'nsec') {
privkey = nip19.decode(privkey).data as string;
}
if (typeof getPublicKey(privkey) === 'string') {
const pubkey = getPublicKey(privkey);
const npub = nip19.npubEncode(pubkey);
setTempPrivkey(privkey);
setPubkey(pubkey);
// add account to local database
await db.createAccount(npub, pubkey);
// redirect to step 2 with delay 1.2s
setTimeout(() => navigate('/auth/import/step-2', { replace: true }), 1200);
}
} catch (error) {
setLoading(false);
setError('privkey', {
type: 'custom',
message: 'Private key is invalid, please check again',
});
}
};
// toggle private key
const showPassword = () => {
if (passwordInput === 'password') {
setPasswordInput('text');
} else {
setPasswordInput('password');
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/import');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-4 pb-4">
<h1 className="text-center text-2xl font-semibold text-white">
Import your Nostr key
</h1>
</div>
<div className="flex flex-col gap-4">
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col gap-3">
<div className="flex flex-col gap-1">
<label htmlFor="privkey" className="font-medium text-white">
Insert your nostr private key, in nsec or hex format
</label>
<div className="relative">
<input
{...register('privkey', { required: true, minLength: 32 })}
type={passwordInput}
placeholder="nsec1..."
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3 py-1 text-white backdrop-blur-xl placeholder:text-white/70 focus:outline-none"
/>
<button
type="button"
onClick={() => showPassword()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 backdrop-blur-xl hover:bg-white/20"
>
{passwordInput === 'password' ? (
<EyeOffIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
) : (
<EyeOnIcon className="h-4 w-4 text-white/50 group-hover:text-white" />
)}
</button>
</div>
<span className="text-sm text-red-500">
{errors.privkey && <p>{errors.privkey.message}</p>}
</span>
</div>
<div className="flex items-center justify-center">
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none"
>
{loading ? (
<>
<span className="w-5" />
<span>Importing...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : (
<>
<span className="w-5" />
<span>Continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@ -1,90 +0,0 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
import { User } from '@shared/user';
import { useOnboarding } from '@stores/onboarding';
import { WidgetKinds } from '@stores/widgets';
import { useNostr } from '@utils/hooks/useNostr';
export function ImportStep2Screen() {
const navigate = useNavigate();
const setStep = useOnboarding((state) => state.setStep);
const { db } = useStorage();
const { fetchUserData } = useNostr();
const [loading, setLoading] = useState(false);
const submit = async () => {
try {
// show loading indicator
setLoading(true);
// prefetch data
const user = await fetchUserData();
// create default widget
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
// redirect to next step
if (user.status === 'ok') {
navigate('/auth/onboarding/step-2', { replace: true });
} else {
setLoading(false);
}
} catch (e) {
console.log('error: ', e);
setLoading(false);
}
};
useEffect(() => {
// save current step, if user close app and reopen it
setStep('/auth/import/step-3');
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div className="mb-4 pb-4">
<h1 className="text-center text-2xl font-semibold text-white">
{loading ? 'Downloading...' : 'Your Nostr profile'}
</h1>
</div>
<div className="flex flex-col gap-3">
<div className="rounded-lg border-t border-white/10 bg-white/20 px-3 py-3">
<User pubkey={db.account.pubkey} variant="simple" />
</div>
<div className="flex flex-col gap-2">
<button
type="button"
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none"
onClick={() => submit()}
>
{loading ? (
<>
<span className="w-5" />
<span>It might take a bit, please patient...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : (
<>
<span className="w-5" />
<span>Continue</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
<span className="text-center text-sm text-white/50">
By clicking &apos;Continue&apos;, Lume will download your old relay list and
metadata. It may take a bit
</span>
</div>
</div>
</div>
);
}

View File

@ -19,13 +19,11 @@ export class LumeStorage {
}
public async secureSave(value: string, key?: string) {
return await invoke('secure_save', { key: this.account.pubkey ?? key, value });
return await invoke('secure_save', { key, value });
}
public async secureLoad(key?: string) {
const value: string = await invoke('secure_load', {
key: this.account.pubkey ?? key,
});
const value: string = await invoke('secure_load', { key });
return value;
}

View File

@ -1,5 +1,6 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createRoot } from 'react-dom/client';
import { Toaster } from 'sonner';
import { NDKProvider } from '@libs/ndk/provider';
import { StorageProvider } from '@libs/storage/provider';
@ -15,6 +16,7 @@ root.render(
<QueryClientProvider client={queryClient}>
<StorageProvider>
<NDKProvider>
<Toaster />
<App />
</NDKProvider>
</StorageProvider>

View File

@ -162,18 +162,18 @@ export const User = memo(function User({
loading="lazy"
decoding="async"
style={{ contentVisibility: 'auto' }}
className="h-10 w-10 rounded-lg"
className="h-11 w-11 rounded-lg"
/>
<Avatar.Fallback delayMs={300}>
<img
src={svgURI}
alt={pubkey}
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
className="h-11 w-11 rounded-lg bg-black dark:bg-white"
/>
</Avatar.Fallback>
</Avatar.Root>
<div className="flex w-full flex-col items-start">
<h3 className="max-w-[15rem] truncate font-medium text-neutral-900 dark:text-neutral-100">
<h3 className="max-w-[15rem] truncate text-base font-semibold text-neutral-900 dark:text-neutral-100">
{user?.name || user?.display_name || user?.displayName}
</h3>
<p className="max-w-[10rem] truncate text-sm text-neutral-900 dark:text-neutral-100/70">