extends Window
## First-run identity setup with encrypted vault.
## Password-based: vault.setup (new) or vault.unlock (returning).
## Fallback: restore from raw mnemonic phrase.
enum Step { AUTH, NAME, USERNAME, DONE }
var has_vault: bool = false # Set by keet_service before popup
var existing_username: String = "" # Set by keet_service for legacy identities with username but no vault
var _step: Step = Step.AUTH
var _is_restore: bool = false
var _display_name: String = ""
var _username: String = ""
@onready var step_label: Label = $Margin/VBox/StepLabel
@onready var info_label: RichTextLabel = $Margin/VBox/InfoLabel
@onready var input_field: LineEdit = $Margin/VBox/InputField
@onready var confirm_field: LineEdit = $Margin/VBox/ConfirmField
@onready var status_label: Label = $Margin/VBox/StatusLabel
@onready var alt_btn: Button = $Margin/VBox/AltBtn
@onready var next_btn: Button = $Margin/VBox/BottomBar/NextBtn
func _ready() -> void:
close_requested.connect(hide)
next_btn.pressed.connect(_on_next)
input_field.text_submitted.connect(func(_t): _on_next())
confirm_field.text_submitted.connect(func(_t): _on_next())
alt_btn.pressed.connect(_on_alt_pressed)
_show_step()
func _show_step() -> void:
status_label.text = ""
input_field.visible = false
input_field.text = ""
input_field.secret = false
input_field.placeholder_text = ""
confirm_field.visible = false
confirm_field.text = ""
alt_btn.visible = false
match _step:
Step.AUTH:
if _is_restore:
_show_restore()
elif has_vault:
_show_unlock()
else:
_show_create()
Step.NAME:
step_label.text = "Step 2 of 3"
info_label.text = "Choose a display name.\n\nThis is how others see you in chat rooms."
input_field.visible = true
input_field.placeholder_text = "Display name (e.g. Fleeky)"
input_field.grab_focus()
next_btn.text = "Set Name"
Step.USERNAME:
step_label.text = "Step 3 of 3"
info_label.text = "Register a unique username.\n\nMust end with a number (e.g. fleeky0, alice42).\nThis cannot be changed later."
input_field.visible = true
input_field.placeholder_text = "Username (e.g. fleeky0)"
input_field.grab_focus()
next_btn.text = "Register"
Step.DONE:
step_label.text = "Done!"
info_label.text = "Identity setup complete.\n\nYou can now join rooms and chat."
next_btn.text = "Start"
func _show_unlock() -> void:
step_label.text = "Step 1 of 3"
info_label.text = "Welcome back!\n\nEnter your password to unlock your identity."
input_field.visible = true
input_field.secret = true
input_field.placeholder_text = "Password"
input_field.grab_focus()
next_btn.text = "Unlock"
alt_btn.visible = true
alt_btn.text = "Restore from mnemonic"
func _show_create() -> void:
var is_vault_only: bool = existing_username != ""
step_label.text = "Vault Setup" if is_vault_only else "Step 1 of 3"
if is_vault_only:
info_label.text = "Secure your existing identity with a password.\n\nThis encrypts your recovery phrase locally."
else:
info_label.text = "Create a password to protect your identity.\n\nYour cryptographic keys are generated and encrypted locally. No servers, no accounts."
input_field.visible = true
input_field.secret = true
input_field.placeholder_text = "Password"
input_field.grab_focus()
confirm_field.visible = true
next_btn.text = "Secure Identity" if is_vault_only else "Create Identity"
alt_btn.visible = true
alt_btn.text = "Restore from mnemonic"
func _show_restore() -> void:
step_label.text = "Restore Identity"
info_label.text = "Enter your recovery phrase and choose a password to encrypt it."
input_field.visible = true
input_field.placeholder_text = "word1 word2 word3 ... word12"
input_field.grab_focus()
confirm_field.visible = true
confirm_field.placeholder_text = "Password for vault"
next_btn.text = "Restore"
alt_btn.visible = true
alt_btn.text = "Back"
func _on_alt_pressed() -> void:
if _step != Step.AUTH:
return
if _is_restore:
_is_restore = false
else:
_is_restore = true
_show_step()
func _on_next() -> void:
next_btn.disabled = true
status_label.text = "Working..."
match _step:
Step.AUTH:
_handle_auth()
Step.NAME:
var display_name := input_field.text.strip_edges()
if display_name == "":
status_label.text = "Name cannot be empty"
next_btn.disabled = false
return
_display_name = display_name
KeetService.identity.set_display_name(display_name, func(_result):
_step = Step.USERNAME
next_btn.disabled = false
_show_step()
)
Step.USERNAME:
var username := input_field.text.strip_edges()
if username == "":
status_label.text = "Username cannot be empty"
next_btn.disabled = false
return
if not username[-1].is_valid_int():
status_label.text = "Username must end with a number (e.g. fleeky0)"
next_btn.disabled = false
return
KeetService.identity.check_username(username, func(result):
if result is Dictionary and not result.get("available", false):
status_label.text = "Username '%s' is taken" % username
next_btn.disabled = false
return
status_label.text = "Registering (may take ~15s)..."
_username = username
KeetService.identity.register_username(username, func(_reg_result):
_step = Step.DONE
next_btn.disabled = false
_show_step()
)
)
Step.DONE:
# Sync display name and username to identity registry
if _display_name != "" or _username != "":
KeetService.identity_mgr.get_active(func(active):
if active is Dictionary and active.get("id", "") != "":
var fields: Dictionary = {}
if _display_name != "":
fields["name"] = _display_name
if _username != "":
fields["username"] = _username
KeetService.identity_mgr.update_identity(active["id"], fields)
)
hide()
KeetService.identity.get_identity()
KeetService.room_mgr.list_rooms()
func _handle_auth() -> void:
if _is_restore:
_handle_restore()
return
var password := input_field.text
if password.length() < 1:
status_label.text = "Password cannot be empty"
next_btn.disabled = false
return
if has_vault:
# Unlock existing vault
status_label.text = "Unlocking (key derivation takes a moment)..."
KeetService.identity.vault_unlock(password, func(result):
if result is Dictionary and result.get("ok", false) == false:
status_label.text = result.get("error", "Wrong password")
next_btn.disabled = false
return
# Check identity registry for existing name/username
# (registry survives GC — it's outside 0/db/)
KeetService.identity_mgr.get_active(func(active):
var reg_name: String = ""
var reg_username: String = ""
if active is Dictionary:
reg_name = active.get("name", "")
reg_username = active.get("username", "")
if reg_name != "" and reg_username != "":
# Fully set up — restore display name and skip to DONE
_display_name = reg_name
_username = reg_username
status_label.text = "Restoring profile..."
KeetService.identity.set_display_name(reg_name, func(_r):
_step = Step.DONE
next_btn.disabled = false
_show_step()
)
elif reg_name != "":
_display_name = reg_name
KeetService.identity.set_display_name(reg_name, func(_r):
_step = Step.USERNAME
next_btn.disabled = false
_show_step()
)
else:
_step = Step.NAME
next_btn.disabled = false
_show_step()
)
)
else:
# Create new vault — validate confirm field
var confirm := confirm_field.text
if password != confirm:
status_label.text = "Passwords do not match"
next_btn.disabled = false
return
status_label.text = "Creating vault (key derivation takes a moment)..."
KeetService.identity.vault_setup(password, func(result):
if result is Dictionary and result.get("ok", false) == false:
status_label.text = result.get("error", "Vault creation failed")
next_btn.disabled = false
return
# Legacy identity with username — skip NAME and USERNAME, go to DONE
if existing_username != "":
_username = existing_username
_step = Step.DONE
else:
_step = Step.NAME
next_btn.disabled = false
_show_step()
)
func _handle_restore() -> void:
var phrase := input_field.text.strip_edges()
if phrase == "":
status_label.text = "Enter your recovery phrase"
next_btn.disabled = false
return
var words := phrase.split(" ")
if words.size() < 12:
status_label.text = "Recovery phrase must be at least 12 words"
next_btn.disabled = false
return
var password := confirm_field.text
if password.length() < 1:
status_label.text = "Password cannot be empty"
next_btn.disabled = false
return
status_label.text = "Restoring identity..."
KeetService.identity.vault_import(phrase, password, func(result):
if result is Dictionary and result.get("ok", false) == false:
status_label.text = result.get("error", "Restore failed")
next_btn.disabled = false
return
_step = Step.NAME
next_btn.disabled = false
_show_step()
)