Remove CreateAccountModal for generation, generate account directly and show recovery step then unlock modal

This commit is contained in:
Nicolas Cantu 2025-12-28 23:28:57 +01:00
parent 7cfd235a00
commit 107571c378
4 changed files with 159 additions and 103 deletions

View File

@ -4,6 +4,8 @@ import { useAuthorPresentation } from '@/hooks/useAuthorPresentation'
import { ArticleField } from './ArticleField' import { ArticleField } from './ArticleField'
import { ArticleFormButtons } from './ArticleFormButtons' import { ArticleFormButtons } from './ArticleFormButtons'
import { CreateAccountModal } from './CreateAccountModal' import { CreateAccountModal } from './CreateAccountModal'
import { RecoveryStep } from './CreateAccountModalSteps'
import { UnlockAccountModal } from './UnlockAccountModal'
import { ImageUploadField } from './ImageUploadField' import { ImageUploadField } from './ImageUploadField'
import { PresentationFormHeader } from './PresentationFormHeader' import { PresentationFormHeader } from './PresentationFormHeader'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
@ -254,17 +256,39 @@ function NoAccountActionButtons({
} }
function NoAccountView() { function NoAccountView() {
const [showCreateModal, setShowCreateModal] = useState(false) const [showImportModal, setShowImportModal] = useState(false)
const [modalStep, setModalStep] = useState<'choose' | 'import'>('choose') 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') => { const handleGenerate = async () => {
setModalStep(step) setGenerating(true)
setShowCreateModal(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 = () => { const handleRecoveryContinue = () => {
setShowCreateModal(false) setShowRecoveryStep(false)
setModalStep('choose') setShowUnlockModal(true)
}
const handleUnlockSuccess = () => {
setShowUnlockModal(false)
setRecoveryPhrase([])
setNpub('')
} }
return ( return (
@ -273,15 +297,35 @@ function NoAccountView() {
<p className="text-center text-cyber-accent mb-2"> <p className="text-center text-cyber-accent mb-2">
Créez un compte ou importez votre clé secrète pour commencer Créez un compte ou importez votre clé secrète pour commencer
</p> </p>
{error && <p className="text-sm text-red-400">{error}</p>}
<NoAccountActionButtons <NoAccountActionButtons
onGenerate={() => handleOpenModal('choose')} onGenerate={handleGenerate}
onImport={() => handleOpenModal('import')} onImport={() => setShowImportModal(true)}
/> />
{showCreateModal && ( {generating && (
<p className="text-cyber-accent text-sm">Génération du compte...</p>
)}
{showImportModal && (
<CreateAccountModal <CreateAccountModal
onSuccess={handleModalClose} onSuccess={() => {
onClose={handleModalClose} setShowImportModal(false)
initialStep={modalStep} setShowUnlockModal(true)
}}
onClose={() => setShowImportModal(false)}
initialStep="import"
/>
)}
{showRecoveryStep && (
<RecoveryStep
recoveryPhrase={recoveryPhrase}
npub={npub}
onContinue={handleRecoveryContinue}
/>
)}
{showUnlockModal && (
<UnlockAccountModal
onSuccess={handleUnlockSuccess}
onClose={() => setShowUnlockModal(false)}
/> />
)} )}
</div> </div>

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useNostrAuth } from '@/hooks/useNostrAuth' import { useNostrAuth } from '@/hooks/useNostrAuth'
import { ConnectedUserMenu } from './ConnectedUserMenu' import { ConnectedUserMenu } from './ConnectedUserMenu'
import { CreateAccountModal } from './CreateAccountModal' import { RecoveryStep } from './CreateAccountModalSteps'
import { UnlockAccountModal } from './UnlockAccountModal' import { UnlockAccountModal } from './UnlockAccountModal'
import type { NostrProfile } from '@/types/nostr' 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(() => { useEffect(() => {
if (accountExists === true && !pubkey && !showCreateModal && !showUnlockModal) { if (accountExists === true && !pubkey && !showRecoveryStep && !showUnlockModal) {
void connect() 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> }) { 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, showUnlockModal,
setShowCreateModal,
setShowUnlockModal, setShowUnlockModal,
onCreateAccount,
}: { }: {
showCreateModal: boolean loading: boolean
error: string | null
showUnlockModal: boolean showUnlockModal: boolean
setShowCreateModal: (show: boolean) => void
setShowUnlockModal: (show: boolean) => void setShowUnlockModal: (show: boolean) => void
onCreateAccount: () => void
}) { }) {
return ( return (
<> <>
{showCreateModal && ( <ConnectForm
<CreateAccountModal onCreateAccount={onCreateAccount}
onSuccess={() => { onUnlock={() => setShowUnlockModal(true)}
setShowCreateModal(false) loading={loading}
setShowUnlockModal(true) error={error}
}} />
onClose={() => setShowCreateModal(false)}
/>
)}
{showUnlockModal && ( {showUnlockModal && (
<UnlockAccountModal <UnlockAccountModal
onSuccess={() => setShowUnlockModal(false)} 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() { export function ConnectButton() {
const { connected, pubkey, profile, loading, error, connect, disconnect, accountExists, isUnlocked } = useNostrAuth() 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 [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) { if (connected && pubkey && isUnlocked) {
return <ConnectedState pubkey={pubkey} profile={profile} loading={loading} disconnect={disconnect} /> return <ConnectedState pubkey={pubkey} profile={profile} loading={loading} disconnect={disconnect} />
@ -160,13 +158,27 @@ export function ConnectButton() {
} }
return ( return (
<DisconnectedState <>
loading={loading} <DisconnectedState
error={error} loading={loading || creatingAccount}
showCreateModal={showCreateModal} error={error || createError}
showUnlockModal={showUnlockModal} showUnlockModal={showUnlockModal}
setShowCreateModal={setShowCreateModal} setShowUnlockModal={setShowUnlockModal}
setShowUnlockModal={setShowUnlockModal} onCreateAccount={handleCreateAccount}
/> />
{showRecoveryStep && (
<RecoveryStep
recoveryPhrase={recoveryPhrase}
npub={npub}
onContinue={handleRecoveryContinue}
/>
)}
{showUnlockModal && (
<UnlockAccountModal
onSuccess={handleUnlockSuccess}
onClose={() => setShowUnlockModal(false)}
/>
)}
</>
) )
} }

View File

@ -1,16 +1,16 @@
export function RecoveryWarning() { export function RecoveryWarning() {
return ( return (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6"> <div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4 mb-6">
<p className="text-yellow-800 font-semibold mb-2"> Important</p> <p className="text-yellow-400 font-semibold mb-2"> Important</p>
<p className="text-yellow-700 text-sm"> <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. 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> <strong className="font-bold"> Ils ne seront jamais affichés à nouveau.</strong>
</p> </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&apos;API Credentials du navigateur. Cette KEK chiffre ensuite votre clé privée stockée dans IndexedDB (système à deux niveaux). Ces mots-clés (dictionnaire BIP39) sont utilisés avec <strong>PBKDF2</strong> pour chiffrer une clé de chiffrement (KEK) stockée dans l&apos;API Credentials du navigateur. Cette KEK chiffre ensuite votre clé privée stockée dans IndexedDB (système à deux niveaux).
</p> </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&apos;accès à votre compte. Notez-les dans un endroit sûr. Sans ces mots-clés, vous perdrez définitivement l&apos;accès à votre compte.
</p> </p>
</div> </div>
@ -27,15 +27,15 @@ export function RecoveryPhraseDisplay({
onCopy: () => void onCopy: () => void
}) { }) {
return ( 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"> <div className="grid grid-cols-2 gap-4 mb-4">
{recoveryPhrase.map((word, index) => ( {recoveryPhrase.map((word, index) => (
<div <div
key={index} 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="text-cyber-accent/70 text-sm mr-2">{index + 1}.</span>
<span className="font-semibold">{word}</span> <span className="font-semibold text-neon-cyan">{word}</span>
</div> </div>
))} ))}
</div> </div>
@ -43,7 +43,7 @@ export function RecoveryPhraseDisplay({
onClick={() => { onClick={() => {
void onCopy() 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'} {copied ? '✓ Copié!' : 'Copier les mots-clés'}
</button> </button>
@ -53,9 +53,9 @@ export function RecoveryPhraseDisplay({
export function PublicKeyDisplay({ npub }: { npub: string }) { export function PublicKeyDisplay({ npub }: { npub: string }) {
return ( return (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6"> <div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4 mb-6">
<p className="text-blue-800 font-semibold mb-2">Votre clé publique (npub)</p> <p className="text-neon-blue font-semibold mb-2">Votre clé publique (npub)</p>
<p className="text-blue-700 text-sm font-mono break-all">{npub}</p> <p className="text-neon-cyan text-sm font-mono break-all">{npub}</p>
</div> </div>
) )
} }
@ -72,7 +72,7 @@ export function ImportKeyForm({
return ( return (
<> <>
<div className="mb-4"> <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) Clé privée (nsec ou hex)
</label> </label>
<textarea <textarea
@ -80,15 +80,15 @@ export function ImportKeyForm({
value={importKey} value={importKey}
onChange={(e) => setImportKey(e.target.value)} onChange={(e) => setImportKey(e.target.value)}
placeholder="nsec1..." 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} rows={4}
/> />
<p className="text-sm text-gray-600 mt-2"> <p className="text-sm text-cyber-accent/70 mt-2">
Après l&apos;import, vous recevrez <strong>4 mots-clés de récupération</strong> (dictionnaire BIP39) pour sécuriser votre compte. Après l&apos;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&apos;API Credentials, qui chiffre ensuite votre clé privée. Ces mots-clés chiffrent une clé de chiffrement (KEK) stockée dans l&apos;API Credentials, qui chiffre ensuite votre clé privée.
</p> </p>
</div> </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"> <div className="flex gap-4">
<button <button
onClick={onBack} 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 Retour
</button> </button>
@ -140,13 +140,13 @@ export function ChooseStepButtons({
<button <button
onClick={onImport} onClick={onImport}
disabled={loading} 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 Importer une clé existante
</button> </button>
<button <button
onClick={onClose} 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 Annuler
</button> </button>

View File

@ -22,8 +22,8 @@ export function RecoveryStep({
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <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"> <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">Sauvegardez vos 4 mots-clés de récupération</h2> <h2 className="text-2xl font-bold mb-4 text-neon-cyan">Sauvegardez vos 4 mots-clés de récupération</h2>
<RecoveryWarning /> <RecoveryWarning />
<RecoveryPhraseDisplay recoveryPhrase={recoveryPhrase} copied={copied} onCopy={handleCopy} /> <RecoveryPhraseDisplay recoveryPhrase={recoveryPhrase} copied={copied} onCopy={handleCopy} />
<PublicKeyDisplay npub={npub} /> <PublicKeyDisplay npub={npub} />
@ -57,8 +57,8 @@ export function ImportStep({
}) { }) {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <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"> <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">Importer une clé privée</h2> <h2 className="text-2xl font-bold mb-4 text-neon-cyan">Importer une clé privée</h2>
<ImportKeyForm importKey={importKey} setImportKey={setImportKey} error={error} /> <ImportKeyForm importKey={importKey} setImportKey={setImportKey} error={error} />
<ImportStepButtons loading={loading} onImport={onImport} onBack={onBack} /> <ImportStepButtons loading={loading} onImport={onImport} onBack={onBack} />
</div> </div>
@ -81,12 +81,12 @@ export function ChooseStep({
}) { }) {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <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"> <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">Créer un compte</h2> <h2 className="text-2xl font-bold mb-4 text-neon-cyan">Créer un compte</h2>
<p className="text-gray-600 mb-6"> <p className="text-cyber-accent/70 mb-6">
Créez un nouveau compte Nostr ou importez une clé privée existante. Créez un nouveau compte Nostr ou importez une clé privée existante.
</p> </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} /> <ChooseStepButtons loading={loading} onGenerate={onGenerate} onImport={onImport} onClose={onClose} />
</div> </div>
</div> </div>