wip
This commit is contained in:
@@ -1,19 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
export function AuthCreateScreen() {
|
||||
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
|
||||
useEffect(() => {
|
||||
if (step) {
|
||||
setPrivkey(tmpPrivkey);
|
||||
}
|
||||
}, [tmpPrivkey]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Outlet />
|
||||
|
||||
@@ -11,13 +11,11 @@ import { useStorage } from '@libs/storage/provider';
|
||||
import { CopyIcon } from '@shared/icons';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
export function CreateStep1Screen() {
|
||||
const { db } = useStorage();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
const setTempPrivkey = useOnboarding((state) => state.setTempPrivkey);
|
||||
const setPubkey = useOnboarding((state) => state.setPubkey);
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
@@ -67,7 +65,6 @@ export function CreateStep1Screen() {
|
||||
setLoading(true);
|
||||
|
||||
// update state
|
||||
setPrivkey(privkey);
|
||||
setTempPrivkey(privkey); // only use if user close app and reopen it
|
||||
setPubkey(pubkey);
|
||||
|
||||
|
||||
@@ -1,122 +1,152 @@
|
||||
import { NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
import { 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 { AvatarUploader } from '@shared/avatarUploader';
|
||||
import { BannerUploader } from '@shared/bannerUploader';
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
import { WidgetKinds } from '@stores/widgets';
|
||||
|
||||
type FormValues = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.password ? values : {},
|
||||
errors: !values.password
|
||||
? {
|
||||
password: {
|
||||
type: 'required',
|
||||
message: 'This is required.',
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function CreateStep2Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
const pubkey = useOnboarding((state) => state.pubkey);
|
||||
const privkey = useStronghold((state) => state.privkey);
|
||||
|
||||
const [passwordInput, setPasswordInput] = useState('password');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [picture, setPicture] = useState('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
||||
const [banner, setBanner] = useState('');
|
||||
|
||||
const { db } = useStorage();
|
||||
|
||||
// toggle private key
|
||||
const showPassword = () => {
|
||||
if (passwordInput === 'password') {
|
||||
setPasswordInput('text');
|
||||
} else {
|
||||
setPasswordInput('password');
|
||||
}
|
||||
};
|
||||
|
||||
const { publish } = useNostr();
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isValid },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = async (data: { [x: string]: string }) => {
|
||||
const onSubmit = async (data: { name: string; about: string; website: string }) => {
|
||||
setLoading(true);
|
||||
if (data.password.length > 3) {
|
||||
// save privkey to secure storage
|
||||
await db.secureSave(pubkey, privkey);
|
||||
try {
|
||||
const profile = {
|
||||
...data,
|
||||
name: data.name,
|
||||
display_name: data.name,
|
||||
bio: data.about,
|
||||
website: data.website,
|
||||
};
|
||||
|
||||
// redirect to next step
|
||||
navigate('/auth/create/step-3', { replace: true });
|
||||
} else {
|
||||
setLoading(false);
|
||||
setError('password', {
|
||||
type: 'custom',
|
||||
message: 'Password is required and must be greater than 3',
|
||||
const event = await publish({
|
||||
content: JSON.stringify(profile),
|
||||
kind: NDKKind.Metadata,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
// create default widget
|
||||
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
|
||||
|
||||
if (event) {
|
||||
navigate('/auth/onboarding', { replace: true });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error: ', e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/create/step-2');
|
||||
setStep('/auth/create/step-3');
|
||||
}, []);
|
||||
|
||||
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">
|
||||
Set password to secure your key
|
||||
Personalize your Nostr profile
|
||||
</h1>
|
||||
<p className="text-white/70">
|
||||
Password is not related to your Nostr account. It is only used to secure your
|
||||
keys stored on your local machine and to unlock the app (like unlocking your
|
||||
phone with a passcode). When you move to other Nostr clients, you just need to
|
||||
copy your private key.
|
||||
Nostr profile is synchronous across all Nostr clients. If you create a profile
|
||||
on Lume, it will also work well with other Nostr clients. If you update your
|
||||
profile on another Nostr client, it will also sync to Lume.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register('password', { required: true })}
|
||||
type={passwordInput}
|
||||
placeholder="Enter password"
|
||||
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-center tracking-widest text-white !outline-none backdrop-blur-xl placeholder:tracking-normal placeholder:text-white/70"
|
||||
/>
|
||||
<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 className="w-full overflow-hidden rounded-xl bg-white/10 backdrop-blur-xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||
<input type={'hidden'} {...register('picture')} value={picture} />
|
||||
<input type={'hidden'} {...register('banner')} value={banner} />
|
||||
<div className="relative">
|
||||
<div className="relative h-36 w-full bg-white/10 backdrop-blur-xl">
|
||||
{banner ? (
|
||||
<Image
|
||||
src={banner}
|
||||
alt="user's banner"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full bg-white/20" />
|
||||
)}
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<BannerUploader setBanner={setBanner} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5 px-4">
|
||||
<div className="relative z-10 -mt-8 h-16 w-16">
|
||||
<Image
|
||||
src={picture}
|
||||
alt="user's avatar"
|
||||
className="h-16 w-16 rounded-lg object-cover ring-2 ring-white/20"
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<AvatarUploader setPicture={setPicture} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-red-400">
|
||||
{errors.password && <p>{errors.password.message}</p>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="name" className="font-medium text-white">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('name', {
|
||||
required: true,
|
||||
minLength: 1,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-12 w-full rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="about" className="font-medium text-white">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register('about')}
|
||||
spellCheck={false}
|
||||
className="relative h-20 w-full resize-none rounded-lg bg-white/20 px-3 py-2 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="website" className="font-medium text-white">
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
{...register('website', {
|
||||
required: false,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-12 w-full rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
@@ -125,7 +155,7 @@ export function CreateStep2Screen() {
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Securing your account...</span>
|
||||
<span>Creating...</span>
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import { NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { AvatarUploader } from '@shared/avatarUploader';
|
||||
import { BannerUploader } from '@shared/bannerUploader';
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { WidgetKinds } from '@stores/widgets';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function CreateStep3Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [picture, setPicture] = useState('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
||||
const [banner, setBanner] = useState('');
|
||||
|
||||
const { db } = useStorage();
|
||||
const { publish } = useNostr();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = async (data: { name: string; about: string; website: string }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const profile = {
|
||||
...data,
|
||||
name: data.name,
|
||||
display_name: data.name,
|
||||
bio: data.about,
|
||||
website: data.website,
|
||||
};
|
||||
|
||||
const event = await publish({
|
||||
content: JSON.stringify(profile),
|
||||
kind: NDKKind.Metadata,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
// create default widget
|
||||
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
|
||||
|
||||
if (event) {
|
||||
navigate('/auth/onboarding', { replace: true });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error: ', e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/create/step-3');
|
||||
}, []);
|
||||
|
||||
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">
|
||||
Personalize your Nostr profile
|
||||
</h1>
|
||||
<p className="text-white/70">
|
||||
Nostr profile is synchronous across all Nostr clients. If you create a profile
|
||||
on Lume, it will also work well with other Nostr clients. If you update your
|
||||
profile on another Nostr client, it will also sync to Lume.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full overflow-hidden rounded-xl bg-white/10 backdrop-blur-xl">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0 flex flex-col">
|
||||
<input type={'hidden'} {...register('picture')} value={picture} />
|
||||
<input type={'hidden'} {...register('banner')} value={banner} />
|
||||
<div className="relative">
|
||||
<div className="relative h-36 w-full bg-white/10 backdrop-blur-xl">
|
||||
{banner ? (
|
||||
<Image
|
||||
src={banner}
|
||||
alt="user's banner"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full bg-white/20" />
|
||||
)}
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<BannerUploader setBanner={setBanner} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5 px-4">
|
||||
<div className="relative z-10 -mt-8 h-16 w-16">
|
||||
<Image
|
||||
src={picture}
|
||||
alt="user's avatar"
|
||||
className="h-16 w-16 rounded-lg object-cover ring-2 ring-white/20"
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<AvatarUploader setPicture={setPicture} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="name" className="font-medium text-white">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('name', {
|
||||
required: true,
|
||||
minLength: 1,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-12 w-full rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="about" className="font-medium text-white">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register('about')}
|
||||
spellCheck={false}
|
||||
className="relative h-20 w-full resize-none rounded-lg bg-white/20 px-3 py-2 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="website" className="font-medium text-white">
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
{...register('website', {
|
||||
required: false,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-12 w-full rounded-lg bg-white/20 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/70"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg border-t border-white/10 bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Creating...</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>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
export function AuthImportScreen() {
|
||||
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
|
||||
useEffect(() => {
|
||||
if (step) {
|
||||
setPrivkey(tmpPrivkey);
|
||||
}
|
||||
}, [tmpPrivkey]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Outlet />
|
||||
|
||||
@@ -9,7 +9,6 @@ import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
type FormValues = {
|
||||
privkey: string;
|
||||
@@ -31,13 +30,14 @@ const resolver: Resolver<FormValues> = async (values) => {
|
||||
|
||||
export function ImportStep1Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
const setTempPubkey = useOnboarding((state) => state.setTempPrivkey);
|
||||
const setPubkey = useOnboarding((state) => state.setPubkey);
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
|
||||
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 {
|
||||
@@ -60,8 +60,7 @@ export function ImportStep1Screen() {
|
||||
const pubkey = getPublicKey(privkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
|
||||
setPrivkey(privkey);
|
||||
setTempPubkey(privkey); // only use if user close app and reopen it
|
||||
setTempPrivkey(privkey);
|
||||
setPubkey(pubkey);
|
||||
|
||||
// add account to local database
|
||||
|
||||
@@ -1,143 +1,89 @@
|
||||
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 { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
import { WidgetKinds } from '@stores/widgets';
|
||||
|
||||
type FormValues = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.password ? values : {},
|
||||
errors: !values.password
|
||||
? {
|
||||
password: {
|
||||
type: 'required',
|
||||
message: 'This is required.',
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function ImportStep2Screen() {
|
||||
const navigate = useNavigate();
|
||||
const setStep = useOnboarding((state) => state.setStep);
|
||||
const pubkey = useOnboarding((state) => state.pubkey);
|
||||
const privkey = useStronghold((state) => state.privkey);
|
||||
|
||||
const [passwordInput, setPasswordInput] = useState('password');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { db } = useStorage();
|
||||
const { fetchUserData } = useNostr();
|
||||
|
||||
// toggle private key
|
||||
const showPassword = () => {
|
||||
if (passwordInput === 'password') {
|
||||
setPasswordInput('text');
|
||||
} else {
|
||||
setPasswordInput('password');
|
||||
}
|
||||
};
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isValid },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
const submit = async () => {
|
||||
try {
|
||||
// show loading indicator
|
||||
setLoading(true);
|
||||
|
||||
const onSubmit = async (data: { [x: string]: string }) => {
|
||||
setLoading(true);
|
||||
if (data.password.length > 3) {
|
||||
// save privkey to secure storage
|
||||
await db.secureSave(pubkey, privkey);
|
||||
// prefetch data
|
||||
const user = await fetchUserData();
|
||||
|
||||
// create default widget
|
||||
await db.createWidget(WidgetKinds.other.learnNostr, 'Learn Nostr', '');
|
||||
|
||||
// redirect to next step
|
||||
navigate('/auth/import/step-3', { replace: true });
|
||||
} else {
|
||||
if (user.status === 'ok') {
|
||||
navigate('/auth/onboarding/step-2', { replace: true });
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error: ', e);
|
||||
setLoading(false);
|
||||
setError('password', {
|
||||
type: 'custom',
|
||||
message: 'Password is required and must be greater than 3, please check again',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// save current step, if user close app and reopen it
|
||||
setStep('/auth/import/step-2');
|
||||
setStep('/auth/import/step-3');
|
||||
}, []);
|
||||
|
||||
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">
|
||||
Set password to secure your key
|
||||
<div className="mb-4 pb-4">
|
||||
<h1 className="text-center text-2xl font-semibold text-white">
|
||||
{loading ? 'Downloading...' : 'Your Nostr profile'}
|
||||
</h1>
|
||||
<p className="text-white/70">
|
||||
Password is not related to your Nostr account. It is only used to secure your
|
||||
keys stored on your local machine and to unlock the app (like unlocking your
|
||||
phone with a passcode). When you move to other Nostr clients, you only need to
|
||||
copy your private key.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register('password', { required: true })}
|
||||
type={passwordInput}
|
||||
placeholder="Enter password"
|
||||
className="relative h-12 w-full rounded-lg border-t border-white/10 bg-white/20 px-3.5 py-1 text-center tracking-widest text-white !outline-none backdrop-blur-xl placeholder:tracking-normal placeholder:text-white/70"
|
||||
/>
|
||||
<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-400">
|
||||
{errors.password && <p>{errors.password.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 border-t border-white/10 bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Securing your account...</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 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 'Continue', Lume will download your old relay list and
|
||||
metadata. It may take a bit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 ImportStep3Screen() {
|
||||
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 'Continue', Lume will download your old relay list and
|
||||
metadata. It may take a bit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export function LockScreen() {
|
||||
return (
|
||||
<div
|
||||
className="h-full w-full bg-cover bg-center"
|
||||
style={{ backgroundImage: 'url(/wallpapers/1.png)' }}
|
||||
>
|
||||
<p>TODO</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { appConfigDir } from '@tauri-apps/api/path';
|
||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
||||
import { 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 { useStronghold } from '@stores/stronghold';
|
||||
|
||||
type FormValues = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.password ? values : {},
|
||||
errors: !values.password
|
||||
? {
|
||||
password: {
|
||||
type: 'required',
|
||||
message: 'This is required.',
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
|
||||
export function MigrateScreen() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
|
||||
const [passwordInput, setPasswordInput] = useState('password');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { db } = useStorage();
|
||||
|
||||
// toggle private key
|
||||
const showPassword = () => {
|
||||
if (passwordInput === 'password') {
|
||||
setPasswordInput('text');
|
||||
} else {
|
||||
setPasswordInput('password');
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isValid },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
|
||||
const onSubmit = async (data: { [x: string]: string }) => {
|
||||
setLoading(true);
|
||||
if (data.password.length > 3) {
|
||||
// load private in secure storage
|
||||
try {
|
||||
// save privkey to secure storage
|
||||
const dir = await appConfigDir();
|
||||
const stronghold = await Stronghold.load(`${dir}/lume.stronghold`, data.password);
|
||||
|
||||
if (!db.secureDB) db.secureDB = stronghold;
|
||||
await db.secureSave(db.account.pubkey, db.account.privkey);
|
||||
|
||||
// add privkey to state
|
||||
setPrivkey(db.account.privkey);
|
||||
// remove privkey in db
|
||||
await db.removePrivkey();
|
||||
// clear cache
|
||||
await queryClient.invalidateQueries(['account']);
|
||||
// redirect to home
|
||||
navigate('/', { replace: true });
|
||||
} catch {
|
||||
setLoading(false);
|
||||
setError('password', {
|
||||
type: 'custom',
|
||||
message: 'Wrong password',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setLoading(false);
|
||||
setError('password', {
|
||||
type: 'custom',
|
||||
message: 'Password is required and must be greater than 3',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-white">
|
||||
Upgrade security for your account
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-full rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<div className="mt-1">
|
||||
<p className="text-sm text-white/50">
|
||||
You're using old Lume version which store your private key as
|
||||
plaintext in database, this is huge security risk.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-white/50">
|
||||
To secure your private key, please set a password and Lume will put your
|
||||
private key in secure storage.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-white/50">
|
||||
It is not possible to start the app without applying this step, it is
|
||||
easy and fast!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-white">
|
||||
Set a password to protect your key
|
||||
</span>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register('password', { required: true })}
|
||||
type={passwordInput}
|
||||
placeholder="min. 4 characters"
|
||||
className="relative w-full rounded-lg bg-white/10 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showPassword()}
|
||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-white/10"
|
||||
>
|
||||
{passwordInput === 'password' ? (
|
||||
<EyeOffIcon
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-white/50 group-hover:text-white"
|
||||
/>
|
||||
) : (
|
||||
<EyeOnIcon
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-white/50 group-hover:text-white"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-red-400">
|
||||
{errors.password && <p>{errors.password.message}</p>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="mt-3 inline-flex h-11 w-full items-center justify-center rounded-md bg-blue-500 font-medium text-white hover:bg-blue-600 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
||||
) : (
|
||||
'Continue →'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { useOnboarding } from '@stores/onboarding';
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
export function OnboardingScreen() {
|
||||
const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
|
||||
useEffect(() => {
|
||||
if (step) {
|
||||
setPrivkey(tmpPrivkey);
|
||||
}
|
||||
}, [tmpPrivkey]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Outlet />
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import { appConfigDir } from '@tauri-apps/api/path';
|
||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { useState } from 'react';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
type FormValues = {
|
||||
password: string;
|
||||
privkey: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.password ? values : {},
|
||||
errors: !values.password
|
||||
? {
|
||||
password: {
|
||||
type: 'required',
|
||||
message: 'This is required.',
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
|
||||
export function ResetScreen() {
|
||||
const navigate = useNavigate();
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
|
||||
const [passwordInput, setPasswordInput] = useState('password');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { db } = useStorage();
|
||||
|
||||
// toggle private key
|
||||
const showPassword = () => {
|
||||
if (passwordInput === 'password') {
|
||||
setPasswordInput('text');
|
||||
} else {
|
||||
setPasswordInput('password');
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isValid },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
|
||||
const onSubmit = async (data: { [x: string]: string }) => {
|
||||
setLoading(true);
|
||||
if (data.password.length > 3) {
|
||||
try {
|
||||
let privkey = data.privkey;
|
||||
if (privkey.startsWith('nsec')) {
|
||||
privkey = nip19.decode(privkey).data as string;
|
||||
}
|
||||
|
||||
const tmpPubkey = getPublicKey(privkey);
|
||||
|
||||
if (tmpPubkey !== db.account.pubkey) {
|
||||
setLoading(false);
|
||||
setError('password', {
|
||||
type: 'custom',
|
||||
message:
|
||||
"Private key don't match current account store in database, please check again",
|
||||
});
|
||||
} else {
|
||||
// remove old stronghold
|
||||
await db.secureReset();
|
||||
|
||||
// save privkey to secure storage
|
||||
const dir = await appConfigDir();
|
||||
const stronghold = await Stronghold.load(
|
||||
`${dir}/lume.stronghold`,
|
||||
data.password
|
||||
);
|
||||
|
||||
if (!db.secureDB) db.secureDB = stronghold;
|
||||
await db.secureSave(db.account.pubkey, db.account.privkey);
|
||||
|
||||
// add privkey to state
|
||||
setPrivkey(db.account.privkey);
|
||||
// redirect to home
|
||||
navigate('/auth/unlock', { replace: true });
|
||||
}
|
||||
} catch {
|
||||
setLoading(false);
|
||||
setError('password', {
|
||||
type: 'custom',
|
||||
message: 'Invalid private key',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setLoading(false);
|
||||
setError('password', {
|
||||
type: 'custom',
|
||||
message: 'Password is required and must be greater than 3',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-6 text-center">
|
||||
<h1 className="text-2xl font-semibold text-white">Reset unlock password</h1>
|
||||
</div>
|
||||
<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">
|
||||
Private key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register('privkey', { required: true })}
|
||||
type="text"
|
||||
placeholder="nsec1..."
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="password" className="font-medium text-white">
|
||||
Set a new password to protect your key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register('password', { required: true })}
|
||||
type={passwordInput}
|
||||
placeholder="Min. 4 characters"
|
||||
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"
|
||||
/>
|
||||
<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/10"
|
||||
>
|
||||
{passwordInput === 'password' ? (
|
||||
<EyeOffIcon className="h-5 w-5 text-white/50 group-hover:text-white" />
|
||||
) : (
|
||||
<EyeOnIcon className="h-5 w-5 text-white/50 group-hover:text-white" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-red-400">
|
||||
{errors.password && <p>{errors.password.message}</p>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex h-12 w-full items-center justify-center rounded-md bg-blue-500 font-medium text-white hover:bg-blue-600 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||
) : (
|
||||
'Continue →'
|
||||
)}
|
||||
</button>
|
||||
<Link
|
||||
to="/auth/unlock"
|
||||
className="mt-1 inline-flex h-12 w-full items-center justify-center rounded-lg text-center text-white/70 hover:bg-white/20"
|
||||
>
|
||||
Back
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { appConfigDir } from '@tauri-apps/api/path';
|
||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
||||
import { useState } from 'react';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowRightCircleIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
type FormValues = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.password ? values : {},
|
||||
errors: !values.password
|
||||
? {
|
||||
password: {
|
||||
type: 'required',
|
||||
message: 'This is required.',
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
|
||||
export function UnlockScreen() {
|
||||
const navigate = useNavigate();
|
||||
const setPrivkey = useStronghold((state) => state.setPrivkey);
|
||||
const setWalletConnectURL = useStronghold((state) => state.setWalletConnectURL);
|
||||
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const { db } = useStorage();
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
|
||||
const onSubmit = async (data: { [x: string]: string }) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const dir = await appConfigDir();
|
||||
const stronghold = await Stronghold.load(`${dir}/lume.stronghold`, data.password);
|
||||
|
||||
if (!db.secureDB) db.secureDB = stronghold;
|
||||
|
||||
const privkey = await db.secureLoad(db.account.pubkey);
|
||||
const uri = await db.secureLoad('walletConnectURL', 'nwc');
|
||||
|
||||
if (privkey) setPrivkey(privkey);
|
||||
if (uri) setWalletConnectURL(uri);
|
||||
// redirect to home
|
||||
navigate('/', { replace: true });
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
setError('password', {
|
||||
type: 'custom',
|
||||
message: e,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 inline-flex w-full items-center justify-center">
|
||||
<User pubkey={db.account.pubkey} variant="avatar" />
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="mx-auto mb-0 flex w-2/3 flex-col"
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register('password', { required: true, minLength: 4 })}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter password"
|
||||
className="relative h-12 w-full rounded-lg bg-neutral-300 py-1 text-center tracking-widest text-neutral-900 !outline-none backdrop-blur-xl placeholder:tracking-normal placeholder:text-neutral-500 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded-lg p-1 backdrop-blur-xl hover:bg-black/10 dark:hover:bg-white/10"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOffIcon className="group-hover:text-dark h-5 w-5 text-black/50 dark:text-white/50 dark:group-hover:text-white" />
|
||||
) : (
|
||||
<EyeOnIcon className="group-hover:text-dark h-5 w-5 text-black/50 dark:text-white/50 dark:group-hover:text-white" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex h-10 w-full items-center justify-between gap-2 rounded-lg bg-blue-500 px-6 text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="w-5" />
|
||||
<span>Unlocking...</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>
|
||||
);
|
||||
}
|
||||
@@ -13,13 +13,10 @@ import { useStorage } from '@libs/storage/provider';
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function ChatScreen() {
|
||||
const listRef = useRef<VListHandle>(null);
|
||||
const userPrivkey = useStronghold((state) => state.privkey);
|
||||
|
||||
const { db } = useStorage();
|
||||
const { ndk } = useNDK();
|
||||
@@ -35,7 +32,7 @@ export function ChatScreen() {
|
||||
<ChatMessage
|
||||
message={message}
|
||||
userPubkey={db.account.pubkey}
|
||||
userPrivkey={userPrivkey}
|
||||
userPrivkey={''}
|
||||
self={message.pubkey === db.account.pubkey}
|
||||
/>
|
||||
);
|
||||
@@ -108,7 +105,7 @@ export function ChatScreen() {
|
||||
<ChatForm
|
||||
receiverPubkey={pubkey}
|
||||
userPubkey={db.account.pubkey}
|
||||
userPrivkey={userPrivkey}
|
||||
userPrivkey={''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,6 @@ import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
import { formatCreatedAt } from '@utils/createdAt';
|
||||
import { useProfile } from '@utils/hooks/useProfile';
|
||||
import { displayNpub } from '@utils/shortenKey';
|
||||
@@ -19,8 +17,7 @@ export const ChatListItem = memo(function ChatListItem({ event }: { event: NDKEv
|
||||
const { db } = useStorage();
|
||||
const { status, user } = useProfile(event.pubkey);
|
||||
|
||||
const privkey = useStronghold((state) => state.privkey);
|
||||
const decryptedContent = useDecryptMessage(event, db.account.pubkey, privkey);
|
||||
const decryptedContent = useDecryptMessage(event, db.account.pubkey);
|
||||
|
||||
const createdAt = formatCreatedAt(event.created_at, true);
|
||||
const svgURI =
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { webln } from '@getalby/sdk';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { message } from '@tauri-apps/plugin-dialog';
|
||||
import { WebviewWindow } from '@tauri-apps/plugin-window';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
@@ -14,8 +13,6 @@ import {
|
||||
LoaderIcon,
|
||||
} from '@shared/icons';
|
||||
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
export function NWCAlby() {
|
||||
const { db } = useStorage();
|
||||
|
||||
@@ -23,8 +20,6 @@ export function NWCAlby() {
|
||||
const [isLoading, setIsloading] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
const setWalletConnectURL = useStronghold((state) => state.setWalletConnectURL);
|
||||
|
||||
const initAlby = async () => {
|
||||
try {
|
||||
setIsloading(true);
|
||||
@@ -36,6 +31,7 @@ export function NWCAlby() {
|
||||
const authURL = provider.getAuthorizationUrl({ name: 'Lume' });
|
||||
|
||||
// open auth window
|
||||
/*
|
||||
const webview = new WebviewWindow('alby', {
|
||||
title: 'Connect Alby',
|
||||
url: authURL.href,
|
||||
@@ -45,11 +41,11 @@ export function NWCAlby() {
|
||||
});
|
||||
|
||||
webview.listen('tauri://close-requested', async () => {
|
||||
await db.secureSave('walletConnectURL', walletConnectURL, 'nwc');
|
||||
setWalletConnectURL(walletConnectURL);
|
||||
await db.secureSave('nwc', walletConnectURL);
|
||||
setIsConnected(true);
|
||||
setIsloading(false);
|
||||
});
|
||||
*/
|
||||
} catch (e) {
|
||||
setIsloading(false);
|
||||
await message(e.toString(), { title: 'Connect Alby', type: 'error' });
|
||||
|
||||
@@ -6,8 +6,6 @@ import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { ArrowRightCircleIcon, CancelIcon, LoaderIcon, WorldIcon } from '@shared/icons';
|
||||
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
type FormValues = {
|
||||
uri: string;
|
||||
};
|
||||
@@ -38,8 +36,6 @@ export function NWCOther() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoading, setIsloading] = useState(false);
|
||||
|
||||
const setWalletConnectURL = useStronghold((state) => state.setWalletConnectURL);
|
||||
|
||||
const onSubmit = async (data: { [x: string]: string }) => {
|
||||
try {
|
||||
if (!data.uri.startsWith('nostr+walletconnect:')) {
|
||||
@@ -57,8 +53,7 @@ export function NWCOther() {
|
||||
const params = new URLSearchParams(uriObj.search);
|
||||
|
||||
if (params.has('relay') && params.has('secret')) {
|
||||
await db.secureSave('walletConnectURL', data.uri, 'nwc');
|
||||
setWalletConnectURL(data.uri);
|
||||
await db.secureSave('nwc', data.uri);
|
||||
setIsloading(false);
|
||||
setIsOpen(false);
|
||||
}
|
||||
@@ -95,7 +90,7 @@ export function NWCOther() {
|
||||
</button>
|
||||
</Dialog.Trigger>
|
||||
</div>
|
||||
<Dialog.Portal className="relative z-10">
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import { NWCAlby } from '@app/nwc/components/alby';
|
||||
import { NWCOther } from '@app/nwc/components/other';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { CheckCircleIcon } from '@shared/icons';
|
||||
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
export function NWCScreen() {
|
||||
const { db } = useStorage();
|
||||
|
||||
const [walletConnectURL, setWalletConnectURL] = useStronghold((state) => [
|
||||
state.walletConnectURL,
|
||||
state.setWalletConnectURL,
|
||||
]);
|
||||
const walletConnectURL = 'test';
|
||||
|
||||
const remove = async () => {
|
||||
setWalletConnectURL('');
|
||||
await db.secureSave('walletConnectURL', '', 'nwc');
|
||||
// setWalletConnectURL('');
|
||||
// await db.secureSave('walletConnectURL', '', 'nwc');
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,15 +5,13 @@ import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { EyeOffIcon, EyeOnIcon } from '@shared/icons';
|
||||
|
||||
import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
export function AccountSettingsScreen() {
|
||||
const { db } = useStorage();
|
||||
|
||||
const [privType, setPrivType] = useState('password');
|
||||
const [nsecType, setNsecType] = useState('password');
|
||||
|
||||
const privkey = useStronghold((state) => state.privkey);
|
||||
const privkey = 'todo';
|
||||
const nsec = useMemo(() => nip19.nsecEncode(privkey), [privkey]);
|
||||
|
||||
const showPrivkey = () => {
|
||||
|
||||
Reference in New Issue
Block a user