OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / scripts / identity_setup_dialog.gd

291 lines · gd ref:
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()
	)