// Creating a new identity — the other half of the intro modal.
//
// ⛔ THE WORDING ON THIS SCREEN IS PART OF THE SECURITY MODEL, not copy — the same rule Pair.jsx states,
// for a sharper reason. Pairing is recoverable: the identity exists elsewhere and can be re-sent.
// Creating is not. Two of the things that happen when this button is tapped cannot be undone by anyone:
//
// 1. The recovery words are shown ONCE. They are the only thing that can rotate this identity's keys,
// which is the only real revocation there is. Losing them is permanent, and no support channel,
// backup or other device can reproduce them.
// 2. Registering the identity appends to a global, public, append-only log. There is no delete. A
// test identity is forever, and this screen must not let anyone find that out afterwards.
//
// So the screen says both, before anything happens — and it does not offer a "skip" past the words.
//
// What it deliberately does NOT ask for: a handle, or which server you are on. Creation is P2P-only —
// the identity names nobody's infrastructure, and a domain handle arrives later, if you accept an
// invite. That is the whole point of the split (hyper-social #54 §8): creating an identity should not
// require choosing whose platform you are on.
import React, { useState } from 'react'
import { View, Text, TextInput, Pressable, ActivityIndicator, StyleSheet, ScrollView } from 'react-native'
export default function Create ({ client, onCreated, onCancel = null }) {
const [pass, setPass] = useState('')
const [confirm, setConfirm] = useState('')
const [busy, setBusy] = useState(false)
const [step, setStep] = useState(null)
const [err, setErr] = useState(null)
// The result, held here so the words can be shown BEFORE the node restarts. Restarting first would
// tear this component down with the only copy of the mnemonic inside it.
const [made, setMade] = useState(null)
const [wroteItDown, setWroteItDown] = useState(false)
const short = pass.length > 0 && pass.length < 8
const mismatch = confirm.length > 0 && pass !== confirm
const ready = pass.length >= 8 && pass === confirm && !busy
const create = async () => {
setBusy(true); setErr(null); setStep('creating your identity…')
const onLog = (m) => { const s = m.msg || String(m); if (s.startsWith('mint:')) setStep(s.slice(5).trim()) }
client.on('log', onLog)
try {
// The worklet owns all of it — keys, the vault, the directory call. This screen never touches a
// secret except to display the words it is handed back.
const r = await client.request('mint', { passphrase: pass })
setMade(r)
setStep(null)
} catch (e) {
setErr(e.message)
setStep(null)
} finally {
if (typeof client.off === 'function') client.off('log', onLog)
setBusy(false)
}
}
// ── PHASE 2: the words ───────────────────────────────────────────────────────────────────────────
// Reached only on success. There is no route back to the form from here, and no way past without the
// acknowledgement — because this is the only time these words exist on screen.
if (made) {
const words = String(made.mnemonic || '').split(/\s+/).filter(Boolean)
return (
<ScrollView style={s.screen} contentContainerStyle={s.wrap}>
<Text style={s.h1}>Write these down</Text>
<View style={s.danger}>
<Text style={s.dangerH}>This is shown once and never again</Text>
<Text style={s.dangerB}>
These {words.length} words are the only way to recover this identity or to remove a lost
device from it. They are not stored anywhere you can read them back. If you lose them, nobody
can help — not us, not another device.
</Text>
<Text style={s.dangerB}>Write them on paper. Do not screenshot them.</Text>
</View>
<View style={s.words}>
{words.map((w, i) => (
<Text key={i} style={s.word}><Text style={s.wordN}>{i + 1}. </Text>{w}</Text>
))}
</View>
<Text style={s.label}>Your identity</Text>
<Text style={s.code}>{made.did}</Text>
{/* HONEST ABOUT WHAT ACTUALLY HAPPENED. Each of these can fail independently, and a screen that
reported success regardless would be the worst possible lie on this particular page. */}
{made.sealed
? <Text style={s.dim}>A copy is sealed with your passphrase, so another device can recover it.</Text>
: <Text style={s.err}>⚠ The sealed backup could not be written{made.sealError ? ' (' + made.sealError + ')' : ''} — the words above are your ONLY copy.</Text>}
{made.submitted
? <Text style={s.dim}>Registered in the public directory, so others can find it.</Text>
: <Text style={s.dim}>Not registered publicly yet{made.submitError ? ' (' + made.submitError + ')' : ''} — it works between your devices now, and can be registered later.</Text>}
<Pressable style={s.checkRow} onPress={() => setWroteItDown((v) => !v)}>
<View style={[s.box, wroteItDown && s.boxOn]}>{wroteItDown ? <Text style={s.tick}>✓</Text> : null}</View>
<Text style={s.checkT}>I have written these words down</Text>
</Pressable>
<Pressable
style={[s.btn, !wroteItDown && s.btnOff]}
disabled={!wroteItDown}
onPress={() => onCreated(made)}
>
<Text style={s.btnT}>Done — restart and start posting</Text>
</Pressable>
</ScrollView>
)
}
// ── PHASE 1: what it costs, and the passphrase ───────────────────────────────────────────────────
return (
<ScrollView style={s.screen} contentContainerStyle={s.wrap}>
<Text style={s.h1}>Create a new identity</Text>
<Text style={s.body}>
This makes a brand-new account that lives on this phone. It is yours — no server owns it, and
nothing here asks whose platform you are joining.
</Text>
<View style={s.warn}>
<Text style={s.warnH}>Two things you cannot undo</Text>
<Text style={s.warnB}>
You will be shown a list of recovery words <Text style={s.b}>once</Text>. They are the only way
to recover this identity, or to cut off a device you lose. Nobody can reproduce them for you.
</Text>
<Text style={s.warnB}>
Creating an identity writes a permanent public record. It cannot be deleted afterwards, so a
throwaway account is not throwaway.
</Text>
</View>
<Text style={s.label}>Passphrase</Text>
<Text style={s.dim}>Used to seal a backup copy of your recovery words, so another device can
restore them. It is never stored — if you forget it, the sealed copy is unopenable and only the
written words remain.</Text>
<TextInput
style={s.input}
value={pass}
onChangeText={setPass}
placeholder="at least 8 characters"
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
/>
<TextInput
style={s.input}
value={confirm}
onChangeText={setConfirm}
placeholder="type it again"
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
/>
{short ? <Text style={s.err}>Too short — at least 8 characters.</Text> : null}
{mismatch ? <Text style={s.err}>The two do not match.</Text> : null}
<Pressable style={[s.btn, !ready && s.btnOff]} disabled={!ready} onPress={create}>
{busy ? <ActivityIndicator color="#fff" /> : <Text style={s.btnT}>Create my identity</Text>}
</Pressable>
{step ? <Text style={s.dim}>{step}</Text> : null}
{err ? <Text style={s.err}>{err}</Text> : null}
{onCancel ? (
<Pressable style={s.skip} disabled={busy} onPress={onCancel}>
<Text style={[s.skipT, busy && s.skipOff]}>Back</Text>
</Pressable>
) : null}
<Text style={s.foot}>
Already have an identity on another device? Go back and pair instead — pairing copies the one you
have, and does not create a second.
</Text>
</ScrollView>
)
}
// ⛔ PAINTS ITS OWN BACKGROUND — see Pair.jsx for the full reason. AndroidManifest sets
// Theme.AppCompat.DayNight.NoActionBar, so an unpainted view sits on a DARK window in dark mode and
// every colour here was chosen for white. On THIS screen that is not merely ugly: the recovery words
// are shown exactly once, and a user who cannot read them has lost the identity permanently.
const s = StyleSheet.create({
screen: { flex: 1, backgroundColor: '#fff' },
wrap: { padding: 24, gap: 10, backgroundColor: '#fff' },
h1: { fontSize: 22, fontWeight: '700', marginBottom: 4 },
body: { fontSize: 15, lineHeight: 21, color: '#333' },
b: { fontWeight: '700' },
warn: { backgroundColor: '#fff6e5', borderColor: '#e8c98a', borderWidth: 1, borderRadius: 8, padding: 14, gap: 8, marginVertical: 10 },
warnH: { fontWeight: '700' },
warnB: { fontSize: 14, lineHeight: 20, color: '#4a3c1a' },
danger: { backgroundColor: '#fdecec', borderColor: '#e0a0a0', borderWidth: 1, borderRadius: 8, padding: 14, gap: 8, marginVertical: 10 },
dangerH: { fontWeight: '700', color: '#7a1f1f' },
dangerB: { fontSize: 14, lineHeight: 20, color: '#7a1f1f' },
words: { backgroundColor: '#f7f7f7', borderRadius: 8, padding: 14, gap: 4, marginVertical: 6 },
word: { fontFamily: 'monospace', fontSize: 15, lineHeight: 22, color: '#14171a' },
wordN: { color: '#5b6b78' },
label: { fontWeight: '600', marginTop: 10, color: '#14171a' },
code: { fontFamily: 'monospace', backgroundColor: '#f2f2f2', padding: 10, borderRadius: 6, fontSize: 12, color: '#14171a' },
input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 6, padding: 10, fontSize: 14, marginTop: 6, color: '#14171a', backgroundColor: '#fff' },
btn: { backgroundColor: '#111', borderRadius: 8, padding: 14, alignItems: 'center', marginTop: 16 },
btnOff: { opacity: 0.4 },
btnT: { color: '#fff', fontWeight: '600' },
checkRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 16 },
box: { width: 24, height: 24, borderWidth: 2, borderColor: '#888', borderRadius: 4, alignItems: 'center', justifyContent: 'center' },
boxOn: { borderColor: '#111', backgroundColor: '#111' },
tick: { color: '#fff', fontWeight: '700' },
checkT: { fontSize: 15, flexShrink: 1, color: '#14171a' },
dim: { color: '#5b6b78', fontSize: 13 },
err: { color: '#b00', marginTop: 8 },
skip: { alignItems: 'center', paddingVertical: 14, marginTop: 6 },
skipT: { color: '#2563eb', fontSize: 16 },
skipOff: { opacity: 0.4 },
foot: { color: '#5b6b78', fontSize: 13, marginTop: 20, lineHeight: 19 }
})