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 { 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>

View File

@ -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)}
/>
)}
</>
)
}

View File

@ -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&apos;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&apos;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&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.
</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>

View File

@ -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>