Remove CreateAccountModal for generation, generate account directly and show recovery step then unlock modal
This commit is contained in:
parent
7cfd235a00
commit
107571c378
@ -4,6 +4,8 @@ import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
|
||||
import { ArticleField } from './ArticleField'
|
||||
import { ArticleFormButtons } from './ArticleFormButtons'
|
||||
import { CreateAccountModal } from './CreateAccountModal'
|
||||
import { RecoveryStep } from './CreateAccountModalSteps'
|
||||
import { UnlockAccountModal } from './UnlockAccountModal'
|
||||
import { ImageUploadField } from './ImageUploadField'
|
||||
import { PresentationFormHeader } from './PresentationFormHeader'
|
||||
import { t } from '@/lib/i18n'
|
||||
@ -254,17 +256,39 @@ function NoAccountActionButtons({
|
||||
}
|
||||
|
||||
function NoAccountView() {
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [modalStep, setModalStep] = useState<'choose' | 'import'>('choose')
|
||||
const [showImportModal, setShowImportModal] = useState(false)
|
||||
const [showRecoveryStep, setShowRecoveryStep] = useState(false)
|
||||
const [showUnlockModal, setShowUnlockModal] = useState(false)
|
||||
const [recoveryPhrase, setRecoveryPhrase] = useState<string[]>([])
|
||||
const [npub, setNpub] = useState('')
|
||||
const [generating, setGenerating] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleOpenModal = (step: 'choose' | 'import') => {
|
||||
setModalStep(step)
|
||||
setShowCreateModal(true)
|
||||
const handleGenerate = async () => {
|
||||
setGenerating(true)
|
||||
setError(null)
|
||||
try {
|
||||
const { nostrAuthService } = await import('@/lib/nostrAuth')
|
||||
const result = await nostrAuthService.createAccount()
|
||||
setRecoveryPhrase(result.recoveryPhrase)
|
||||
setNpub(result.npub)
|
||||
setShowRecoveryStep(true)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to create account')
|
||||
} finally {
|
||||
setGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
setShowCreateModal(false)
|
||||
setModalStep('choose')
|
||||
const handleRecoveryContinue = () => {
|
||||
setShowRecoveryStep(false)
|
||||
setShowUnlockModal(true)
|
||||
}
|
||||
|
||||
const handleUnlockSuccess = () => {
|
||||
setShowUnlockModal(false)
|
||||
setRecoveryPhrase([])
|
||||
setNpub('')
|
||||
}
|
||||
|
||||
return (
|
||||
@ -273,15 +297,35 @@ function NoAccountView() {
|
||||
<p className="text-center text-cyber-accent mb-2">
|
||||
Créez un compte ou importez votre clé secrète pour commencer
|
||||
</p>
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
<NoAccountActionButtons
|
||||
onGenerate={() => handleOpenModal('choose')}
|
||||
onImport={() => handleOpenModal('import')}
|
||||
onGenerate={handleGenerate}
|
||||
onImport={() => setShowImportModal(true)}
|
||||
/>
|
||||
{showCreateModal && (
|
||||
{generating && (
|
||||
<p className="text-cyber-accent text-sm">Génération du compte...</p>
|
||||
)}
|
||||
{showImportModal && (
|
||||
<CreateAccountModal
|
||||
onSuccess={handleModalClose}
|
||||
onClose={handleModalClose}
|
||||
initialStep={modalStep}
|
||||
onSuccess={() => {
|
||||
setShowImportModal(false)
|
||||
setShowUnlockModal(true)
|
||||
}}
|
||||
onClose={() => setShowImportModal(false)}
|
||||
initialStep="import"
|
||||
/>
|
||||
)}
|
||||
{showRecoveryStep && (
|
||||
<RecoveryStep
|
||||
recoveryPhrase={recoveryPhrase}
|
||||
npub={npub}
|
||||
onContinue={handleRecoveryContinue}
|
||||
/>
|
||||
)}
|
||||
{showUnlockModal && (
|
||||
<UnlockAccountModal
|
||||
onSuccess={handleUnlockSuccess}
|
||||
onClose={() => setShowUnlockModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||
import { ConnectedUserMenu } from './ConnectedUserMenu'
|
||||
import { CreateAccountModal } from './CreateAccountModal'
|
||||
import { RecoveryStep } from './CreateAccountModalSteps'
|
||||
import { UnlockAccountModal } from './UnlockAccountModal'
|
||||
import type { NostrProfile } from '@/types/nostr'
|
||||
|
||||
@ -37,12 +37,12 @@ function ConnectForm({
|
||||
)
|
||||
}
|
||||
|
||||
function useAutoConnect(accountExists: boolean | null, pubkey: string | null, showCreateModal: boolean, showUnlockModal: boolean, connect: () => Promise<void>) {
|
||||
function useAutoConnect(accountExists: boolean | null, pubkey: string | null, showRecoveryStep: boolean, showUnlockModal: boolean, connect: () => Promise<void>) {
|
||||
useEffect(() => {
|
||||
if (accountExists === true && !pubkey && !showCreateModal && !showUnlockModal) {
|
||||
if (accountExists === true && !pubkey && !showRecoveryStep && !showUnlockModal) {
|
||||
void connect()
|
||||
}
|
||||
}, [accountExists, pubkey, showCreateModal, showUnlockModal, connect])
|
||||
}, [accountExists, pubkey, showRecoveryStep, showUnlockModal, connect])
|
||||
}
|
||||
|
||||
function ConnectedState({ pubkey, profile, loading, disconnect }: { pubkey: string; profile: NostrProfile | null; loading: boolean; disconnect: () => Promise<void> }) {
|
||||
@ -72,28 +72,28 @@ function UnlockState({ loading, error, onUnlock, onClose }: { loading: boolean;
|
||||
)
|
||||
}
|
||||
|
||||
function DisconnectedModals({
|
||||
showCreateModal,
|
||||
|
||||
function DisconnectedState({
|
||||
loading,
|
||||
error,
|
||||
showUnlockModal,
|
||||
setShowCreateModal,
|
||||
setShowUnlockModal,
|
||||
onCreateAccount,
|
||||
}: {
|
||||
showCreateModal: boolean
|
||||
loading: boolean
|
||||
error: string | null
|
||||
showUnlockModal: boolean
|
||||
setShowCreateModal: (show: boolean) => void
|
||||
setShowUnlockModal: (show: boolean) => void
|
||||
onCreateAccount: () => void
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{showCreateModal && (
|
||||
<CreateAccountModal
|
||||
onSuccess={() => {
|
||||
setShowCreateModal(false)
|
||||
setShowUnlockModal(true)
|
||||
}}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
<ConnectForm
|
||||
onCreateAccount={onCreateAccount}
|
||||
onUnlock={() => setShowUnlockModal(true)}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
)}
|
||||
{showUnlockModal && (
|
||||
<UnlockAccountModal
|
||||
onSuccess={() => setShowUnlockModal(false)}
|
||||
@ -104,45 +104,43 @@ function DisconnectedModals({
|
||||
)
|
||||
}
|
||||
|
||||
function DisconnectedState({
|
||||
loading,
|
||||
error,
|
||||
showCreateModal,
|
||||
showUnlockModal,
|
||||
setShowCreateModal,
|
||||
setShowUnlockModal,
|
||||
}: {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
showCreateModal: boolean
|
||||
showUnlockModal: boolean
|
||||
setShowCreateModal: (show: boolean) => void
|
||||
setShowUnlockModal: (show: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<ConnectForm
|
||||
onCreateAccount={() => setShowCreateModal(true)}
|
||||
onUnlock={() => setShowUnlockModal(true)}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
<DisconnectedModals
|
||||
showCreateModal={showCreateModal}
|
||||
showUnlockModal={showUnlockModal}
|
||||
setShowCreateModal={setShowCreateModal}
|
||||
setShowUnlockModal={setShowUnlockModal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConnectButton() {
|
||||
const { connected, pubkey, profile, loading, error, connect, disconnect, accountExists, isUnlocked } = useNostrAuth()
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [showRecoveryStep, setShowRecoveryStep] = useState(false)
|
||||
const [showUnlockModal, setShowUnlockModal] = useState(false)
|
||||
const [recoveryPhrase, setRecoveryPhrase] = useState<string[]>([])
|
||||
const [npub, setNpub] = useState('')
|
||||
const [creatingAccount, setCreatingAccount] = useState(false)
|
||||
const [createError, setCreateError] = useState<string | null>(null)
|
||||
|
||||
useAutoConnect(accountExists, pubkey, showCreateModal, showUnlockModal, connect)
|
||||
useAutoConnect(accountExists, pubkey, false, showUnlockModal, connect)
|
||||
|
||||
const handleCreateAccount = async () => {
|
||||
setCreatingAccount(true)
|
||||
setCreateError(null)
|
||||
try {
|
||||
const { nostrAuthService } = await import('@/lib/nostrAuth')
|
||||
const result = await nostrAuthService.createAccount()
|
||||
setRecoveryPhrase(result.recoveryPhrase)
|
||||
setNpub(result.npub)
|
||||
setShowRecoveryStep(true)
|
||||
} catch (e) {
|
||||
setCreateError(e instanceof Error ? e.message : 'Failed to create account')
|
||||
} finally {
|
||||
setCreatingAccount(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRecoveryContinue = () => {
|
||||
setShowRecoveryStep(false)
|
||||
setShowUnlockModal(true)
|
||||
}
|
||||
|
||||
const handleUnlockSuccess = () => {
|
||||
setShowUnlockModal(false)
|
||||
setRecoveryPhrase([])
|
||||
setNpub('')
|
||||
}
|
||||
|
||||
if (connected && pubkey && isUnlocked) {
|
||||
return <ConnectedState pubkey={pubkey} profile={profile} loading={loading} disconnect={disconnect} />
|
||||
@ -160,13 +158,27 @@ export function ConnectButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DisconnectedState
|
||||
loading={loading}
|
||||
error={error}
|
||||
showCreateModal={showCreateModal}
|
||||
loading={loading || creatingAccount}
|
||||
error={error || createError}
|
||||
showUnlockModal={showUnlockModal}
|
||||
setShowCreateModal={setShowCreateModal}
|
||||
setShowUnlockModal={setShowUnlockModal}
|
||||
onCreateAccount={handleCreateAccount}
|
||||
/>
|
||||
{showRecoveryStep && (
|
||||
<RecoveryStep
|
||||
recoveryPhrase={recoveryPhrase}
|
||||
npub={npub}
|
||||
onContinue={handleRecoveryContinue}
|
||||
/>
|
||||
)}
|
||||
{showUnlockModal && (
|
||||
<UnlockAccountModal
|
||||
onSuccess={handleUnlockSuccess}
|
||||
onClose={() => setShowUnlockModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
|
||||
export function RecoveryWarning() {
|
||||
return (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||
<p className="text-yellow-800 font-semibold mb-2">⚠️ Important</p>
|
||||
<p className="text-yellow-700 text-sm">
|
||||
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4 mb-6">
|
||||
<p className="text-yellow-400 font-semibold mb-2">⚠️ Important</p>
|
||||
<p className="text-yellow-300/90 text-sm">
|
||||
Ces <strong className="font-bold">4 mots-clés</strong> sont votre seule façon de récupérer votre compte.
|
||||
<strong className="font-bold"> Ils ne seront jamais affichés à nouveau.</strong>
|
||||
</p>
|
||||
<p className="text-yellow-700 text-sm mt-2">
|
||||
<p className="text-yellow-300/90 text-sm mt-2">
|
||||
Ces mots-clés (dictionnaire BIP39) sont utilisés avec <strong>PBKDF2</strong> pour chiffrer une clé de chiffrement (KEK) stockée dans l'API Credentials du navigateur. Cette KEK chiffre ensuite votre clé privée stockée dans IndexedDB (système à deux niveaux).
|
||||
</p>
|
||||
<p className="text-yellow-700 text-sm mt-2">
|
||||
<p className="text-yellow-300/90 text-sm mt-2">
|
||||
Notez-les dans un endroit sûr. Sans ces mots-clés, vous perdrez définitivement l'accès à votre compte.
|
||||
</p>
|
||||
</div>
|
||||
@ -27,15 +27,15 @@ export function RecoveryPhraseDisplay({
|
||||
onCopy: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-gray-50 border border-gray-300 rounded-lg p-6 mb-6">
|
||||
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6 mb-6">
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
{recoveryPhrase.map((word, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white border border-gray-300 rounded-lg p-3 text-center font-mono text-lg"
|
||||
className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-3 text-center font-mono text-lg"
|
||||
>
|
||||
<span className="text-gray-500 text-sm mr-2">{index + 1}.</span>
|
||||
<span className="font-semibold">{word}</span>
|
||||
<span className="text-cyber-accent/70 text-sm mr-2">{index + 1}.</span>
|
||||
<span className="font-semibold text-neon-cyan">{word}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -43,7 +43,7 @@ export function RecoveryPhraseDisplay({
|
||||
onClick={() => {
|
||||
void onCopy()
|
||||
}}
|
||||
className="w-full py-2 px-4 bg-gray-200 hover:bg-gray-300 rounded-lg text-sm font-medium transition-colors"
|
||||
className="w-full py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
{copied ? '✓ Copié!' : 'Copier les mots-clés'}
|
||||
</button>
|
||||
@ -53,9 +53,9 @@ export function RecoveryPhraseDisplay({
|
||||
|
||||
export function PublicKeyDisplay({ npub }: { npub: string }) {
|
||||
return (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<p className="text-blue-800 font-semibold mb-2">Votre clé publique (npub)</p>
|
||||
<p className="text-blue-700 text-sm font-mono break-all">{npub}</p>
|
||||
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4 mb-6">
|
||||
<p className="text-neon-blue font-semibold mb-2">Votre clé publique (npub)</p>
|
||||
<p className="text-neon-cyan text-sm font-mono break-all">{npub}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -72,7 +72,7 @@ export function ImportKeyForm({
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="importKey" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label htmlFor="importKey" className="block text-sm font-medium text-cyber-accent mb-2">
|
||||
Clé privée (nsec ou hex)
|
||||
</label>
|
||||
<textarea
|
||||
@ -80,15 +80,15 @@ export function ImportKeyForm({
|
||||
value={importKey}
|
||||
onChange={(e) => setImportKey(e.target.value)}
|
||||
placeholder="nsec1..."
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono text-sm"
|
||||
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/30 rounded-lg font-mono text-sm text-neon-cyan"
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
<p className="text-sm text-cyber-accent/70 mt-2">
|
||||
Après l'import, vous recevrez <strong>4 mots-clés de récupération</strong> (dictionnaire BIP39) pour sécuriser votre compte.
|
||||
Ces mots-clés chiffrent une clé de chiffrement (KEK) stockée dans l'API Credentials, qui chiffre ensuite votre clé privée.
|
||||
</p>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-600 mb-4">{error}</p>}
|
||||
{error && <p className="text-sm text-red-400 mb-4">{error}</p>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -98,7 +98,7 @@ export function ImportStepButtons({ loading, onImport, onBack }: { loading: bool
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex-1 py-2 px-4 bg-gray-200 hover:bg-gray-300 rounded-lg font-medium transition-colors"
|
||||
className="flex-1 py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors"
|
||||
>
|
||||
Retour
|
||||
</button>
|
||||
@ -140,13 +140,13 @@ export function ChooseStepButtons({
|
||||
<button
|
||||
onClick={onImport}
|
||||
disabled={loading}
|
||||
className="w-full py-3 px-6 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition-colors disabled:opacity-50"
|
||||
className="w-full py-3 px-6 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors disabled:opacity-50"
|
||||
>
|
||||
Importer une clé existante
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full py-2 px-4 text-gray-500 hover:text-gray-700 font-medium transition-colors"
|
||||
className="w-full py-2 px-4 text-cyber-accent/70 hover:text-neon-cyan font-medium transition-colors"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
|
||||
@ -22,8 +22,8 @@ export function RecoveryStep({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<h2 className="text-2xl font-bold mb-4">Sauvegardez vos 4 mots-clés de récupération</h2>
|
||||
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto shadow-glow-cyan">
|
||||
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">Sauvegardez vos 4 mots-clés de récupération</h2>
|
||||
<RecoveryWarning />
|
||||
<RecoveryPhraseDisplay recoveryPhrase={recoveryPhrase} copied={copied} onCopy={handleCopy} />
|
||||
<PublicKeyDisplay npub={npub} />
|
||||
@ -57,8 +57,8 @@ export function ImportStep({
|
||||
}) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
||||
<h2 className="text-2xl font-bold mb-4">Importer une clé privée</h2>
|
||||
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 shadow-glow-cyan">
|
||||
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">Importer une clé privée</h2>
|
||||
<ImportKeyForm importKey={importKey} setImportKey={setImportKey} error={error} />
|
||||
<ImportStepButtons loading={loading} onImport={onImport} onBack={onBack} />
|
||||
</div>
|
||||
@ -81,12 +81,12 @@ export function ChooseStep({
|
||||
}) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
||||
<h2 className="text-2xl font-bold mb-4">Créer un compte</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
<div className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-6 max-w-md w-full mx-4 shadow-glow-cyan">
|
||||
<h2 className="text-2xl font-bold mb-4 text-neon-cyan">Créer un compte</h2>
|
||||
<p className="text-cyber-accent/70 mb-6">
|
||||
Créez un nouveau compte Nostr ou importez une clé privée existante.
|
||||
</p>
|
||||
{error && <p className="text-sm text-red-600 mb-4">{error}</p>}
|
||||
{error && <p className="text-sm text-red-400 mb-4">{error}</p>}
|
||||
<ChooseStepButtons loading={loading} onGenerate={onGenerate} onImport={onImport} onClose={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user