diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 79ec3cd..e738762 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -42,7 +42,9 @@ var poll_timer: Timer var mode := "login" var reset_requested := false var steam_transaction_id := "" -var steam_poll_attempts := 0 +var steam_poll_deadline_msec := 0 +var steam_poll_failures := 0 +var steam_popup_opened := false var automatic_login_pending := false var login_attempt_username := "" var compact_layout := false @@ -73,6 +75,7 @@ func _ready() -> void: func on_show(payload: Variant = null) -> void: + _reset_steam_flow() _set_busy(false) _set_mode("login") _clear_message() @@ -84,7 +87,7 @@ func on_show(payload: Variant = null) -> void: func on_hide() -> void: - poll_timer.stop() + _reset_steam_flow() func _build_ui() -> void: @@ -407,6 +410,8 @@ func _add_dot_grid(origin: Vector2) -> void: func _set_mode(next_mode: String) -> void: + if mode == "steam" and next_mode != "steam": + _reset_steam_flow() mode = next_mode if login_view == null: return login_view.visible = mode == "login" @@ -601,23 +606,30 @@ func _on_reset_confirmed(result: int, code: int, _headers: PackedStringArray, bo func _start_steam() -> void: if OS.has_feature("web"): - JavaScriptBridge.eval("window.medmindSteamWindow = window.open('about:blank', 'medmind_steam');", true) + var popup_result: Variant = JavaScriptBridge.eval("window.ranazeSteamWindow = window.open('about:blank', 'ranaze_steam'); window.ranazeSteamWindow !== null;", true) + steam_popup_opened = bool(popup_result) + if not steam_popup_opened: + return _show_error("Steam sign in needs a popup. Allow popups for this site, then try again.") _send_json("steam_start", "/steam/start", {}, "Opening Steam...") func _on_steam_started(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: _set_busy(false) if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300: + _close_steam_popup() return _show_error("Unable to start Steam sign in.") var data := _parse_dictionary(body) steam_transaction_id = str(data.get("transactionId", "")) var auth_url := str(data.get("authUrl", "")) - if steam_transaction_id.is_empty() or auth_url.is_empty(): return _show_error("Steam returned an invalid sign-in response.") + if steam_transaction_id.is_empty() or auth_url.is_empty(): + _close_steam_popup() + return _show_error("Steam returned an invalid sign-in response.") if OS.has_feature("web"): - JavaScriptBridge.eval("if (window.medmindSteamWindow) { window.medmindSteamWindow.location.href = %s; } else { window.location.href = %s; }" % [JSON.stringify(auth_url), JSON.stringify(auth_url)], true) + JavaScriptBridge.eval("if (window.ranazeSteamWindow && !window.ranazeSteamWindow.closed) { window.ranazeSteamWindow.location.href = %s; }" % JSON.stringify(auth_url), true) else: OS.shell_open(auth_url) - steam_poll_attempts = 0 + steam_poll_deadline_msec = Time.get_ticks_msec() + 300000 + steam_poll_failures = 0 _show_success("Complete sign in in the Steam window. Waiting for verification...") poll_timer.start() @@ -625,21 +637,32 @@ func _on_steam_started(result: int, code: int, _headers: PackedStringArray, body func _poll_steam() -> void: var status_request: HTTPRequest = requests["steam_status"] if steam_transaction_id.is_empty() or status_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: return - steam_poll_attempts += 1 - if steam_poll_attempts > 150: - poll_timer.stop(); return _show_error("Steam sign in timed out. Try again when ready.") + if steam_poll_deadline_msec > 0 and Time.get_ticks_msec() >= steam_poll_deadline_msec: + _reset_steam_flow() + return _show_error("Steam sign in timed out. Try again when ready.") var error: Error = status_request.request("%s/steam/status/%s" % [auth_base_url, steam_transaction_id]) - if error != OK: _show_error("Waiting for a stable connection to Steam...") + if error != OK: + steam_poll_failures += 1 + if steam_poll_failures >= 3: _show_error("Connection interrupted. Still waiting for Steam...") func _on_steam_status(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: - if result != HTTPRequest.RESULT_SUCCESS or code >= 500: return + if result != HTTPRequest.RESULT_SUCCESS or code >= 500 or code == 429: + steam_poll_failures += 1 + if steam_poll_failures >= 3: _show_error("Connection interrupted. Still waiting for Steam...") + return if code == 404: - poll_timer.stop(); return _show_error("Steam sign in expired. Please try again.") + _reset_steam_flow() + return _show_error("Steam sign in expired. Please try again.") + if code < 200 or code >= 300: + _reset_steam_flow() + return _show_error(_response_message(body, "Steam sign in could not be completed.")) + steam_poll_failures = 0 var data := _parse_dictionary(body) match str(data.get("status", "pending")): - "requires_account": poll_timer.stop(); _set_mode("steam") - "complete": poll_timer.stop(); _accept_session(body, str(data.get("username", ""))) + "pending": _show_success("Complete sign in in the Steam window. Waiting for verification...") + "requires_account": poll_timer.stop(); _close_steam_popup(); _set_mode("steam") + "complete": poll_timer.stop(); _close_steam_popup(); _accept_session(body, str(data.get("username", ""))) func _on_steam_action_changed(index: int) -> void: @@ -665,11 +688,25 @@ func _on_steam_completed(result: int, code: int, _headers: PackedStringArray, bo _set_busy(false) if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300: return _show_error(_response_message(body, "Unable to connect this Steam account.")) - steam_poll_attempts = 0 + steam_poll_failures = 0 poll_timer.start() _poll_steam() +func _reset_steam_flow() -> void: + if poll_timer != null: poll_timer.stop() + steam_transaction_id = "" + steam_poll_deadline_msec = 0 + steam_poll_failures = 0 + _close_steam_popup() + + +func _close_steam_popup() -> void: + if OS.has_feature("web") and steam_popup_opened: + JavaScriptBridge.eval("if (window.ranazeSteamWindow && !window.ranazeSteamWindow.closed) { window.ranazeSteamWindow.close(); } window.ranazeSteamWindow = null;", true) + steam_popup_opened = false + + func _accept_session(body: PackedByteArray, fallback_username: String) -> void: var response := _parse_dictionary(body) var token := str(response.get("accessToken", "")) diff --git a/tests/steam_login_test.gd b/tests/steam_login_test.gd new file mode 100644 index 0000000..7dda5c8 --- /dev/null +++ b/tests/steam_login_test.gd @@ -0,0 +1,54 @@ +extends SceneTree + +var failures := 0 +var login: Control + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + login = load("res://scripts/ui/login_screen.gd").new() + root.add_child(login) + await process_frame + _test_elapsed_timeout_clears_flow() + _test_transient_failure_keeps_transaction() + _test_leaving_completion_cancels_flow() + login.queue_free() + await process_frame + quit(1 if failures > 0 else 0) + + +func _test_elapsed_timeout_clears_flow() -> void: + login.steam_transaction_id = "expired-transaction" + login.steam_poll_deadline_msec = 1 + login.poll_timer.start() + login._poll_steam() + _assert(login.steam_transaction_id.is_empty(), "elapsed Steam login deadline should clear the transaction") + _assert(login.poll_timer.is_stopped(), "elapsed Steam login deadline should stop polling") + + +func _test_transient_failure_keeps_transaction() -> void: + login.steam_transaction_id = "retry-transaction" + login.steam_poll_deadline_msec = Time.get_ticks_msec() + 300000 + login.steam_poll_failures = 2 + login._on_steam_status(HTTPRequest.RESULT_CANT_CONNECT, 0, PackedStringArray(), PackedByteArray()) + _assert(login.steam_transaction_id == "retry-transaction", "temporary connection failures should preserve the Steam transaction") + _assert(login.steam_poll_failures == 3, "temporary connection failures should be tracked without consuming the deadline") + + +func _test_leaving_completion_cancels_flow() -> void: + login.mode = "steam" + login.steam_transaction_id = "cancel-transaction" + login.poll_timer.start() + login._set_mode("login") + _assert(login.steam_transaction_id.is_empty(), "leaving Steam account completion should clear its transaction") + _assert(login.poll_timer.is_stopped(), "leaving Steam account completion should stop polling") + + +func _assert(condition: bool, message: String) -> void: + if condition: + return + failures += 1 + push_error(message) diff --git a/tests/steam_login_test.gd.uid b/tests/steam_login_test.gd.uid new file mode 100644 index 0000000..4cc9978 --- /dev/null +++ b/tests/steam_login_test.gd.uid @@ -0,0 +1 @@ +uid://cnqc7wymmiwdd