Add provider-neutral Google authentication

This commit is contained in:
hz
2026-07-20 21:42:56 -05:00
parent 48e19f138e
commit 72fd1e3e0f
20 changed files with 1084 additions and 36 deletions
+119
View File
@@ -6,6 +6,8 @@ signal preferences_saved
signal password_changed
signal account_deleted
signal archive_saved(path: String)
signal external_link_started(provider: String)
signal external_linked(provider: String)
const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
const AUTH_URL := "https://us3q.ranaze.com/api/Auth"
@@ -15,11 +17,18 @@ var account: Dictionary = {}
var preferences: Dictionary = _default_preferences()
var auth_request: HTTPRequest
var profile_request: HTTPRequest
var external_request: HTTPRequest
var external_poll_timer: Timer
var auth_operation := ""
var profile_operation := ""
var auth_token := ""
var profile_token := ""
var preferences_pending := false
var external_operation := ""
var external_provider := ""
var external_provider_name := ""
var external_transaction_id := ""
var external_poll_deadline_msec := 0
func _ready() -> void:
@@ -31,6 +40,14 @@ func _ready() -> void:
profile_request.timeout = 30.0
profile_request.request_completed.connect(_on_profile_completed)
add_child(profile_request)
external_request = HTTPRequest.new()
external_request.timeout = 25.0
external_request.request_completed.connect(_on_external_completed)
add_child(external_request)
external_poll_timer = Timer.new()
external_poll_timer.wait_time = 2.0
external_poll_timer.timeout.connect(_poll_external_link)
add_child(external_poll_timer)
AuthState.auth_changed.connect(_on_auth_changed)
@@ -83,6 +100,32 @@ func delete_account(password: String, confirmation: String) -> void:
})
func connect_provider(provider: String, provider_name: String) -> void:
if not AuthState.can_send_authenticated_request():
request_failed.emit("Sign in again before connecting an account.")
return
if external_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
request_failed.emit("Another connection request is still in progress.")
return
external_provider = provider
external_provider_name = provider_name
var storage_key := _external_storage_key()
if OS.has_feature("web"):
var popup_result: Variant = JavaScriptBridge.eval(
"window.localStorage.removeItem(%s); var bridge = new URL('/external-redirect.html?key=' + encodeURIComponent(%s) + '&provider=' + encodeURIComponent(%s), window.location.origin).href; window.ranazeProfileExternalWindow = window.open(bridge, 'ranaze_profile_external', 'popup=yes,width=560,height=720,resizable=yes,scrollbars=yes'); window.ranazeProfileExternalWindow !== null;" % [JSON.stringify(storage_key), JSON.stringify(storage_key), JSON.stringify(provider_name)], true)
if not bool(popup_result):
request_failed.emit("Allow popups for this site, then try again.")
_reset_external_link()
return
external_operation = "start"
var headers := PackedStringArray(["Authorization: Bearer %s" % AuthState.access_token, "Content-Type: application/json"])
var error := external_request.request("%s/external/%s/start" % [_auth_base(), provider], headers,
HTTPClient.METHOD_POST, JSON.stringify({"purpose": "link"}))
if error != OK:
request_failed.emit("The account provider could not be reached.")
_reset_external_link()
func _request_auth(operation: String, method: HTTPClient.Method, path: String, payload: Variant = null) -> void:
if auth_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
request_failed.emit("Another account request is still in progress.")
@@ -162,6 +205,82 @@ func _on_profile_completed(result: int, response_code: int, _headers: PackedStri
_save_archive(JSON.stringify(archive, " "))
func _on_external_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
var operation := external_operation
external_operation = ""
if response_code == 401:
AuthState.handle_unauthorized(AuthState.access_token)
_reset_external_link()
return
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
request_failed.emit(_error_message(body, "The external account request failed."))
_reset_external_link()
return
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
if typeof(parsed) != TYPE_DICTIONARY:
request_failed.emit("The external account service returned invalid data.")
_reset_external_link()
return
var data: Dictionary = parsed
if operation == "start":
external_transaction_id = str(data.get("transactionId", ""))
var auth_url := str(data.get("authUrl", ""))
if external_transaction_id.is_empty() or auth_url.is_empty():
request_failed.emit("The provider returned an invalid response.")
_reset_external_link()
return
if OS.has_feature("web"):
JavaScriptBridge.eval("var url=%s; window.localStorage.setItem(%s,url); if(window.ranazeProfileExternalWindow&&!window.ranazeProfileExternalWindow.closed){window.ranazeProfileExternalWindow.location.replace(url);window.ranazeProfileExternalWindow.focus();}" % [JSON.stringify(auth_url), JSON.stringify(_external_storage_key())], true)
else:
OS.shell_open(auth_url)
external_poll_deadline_msec = Time.get_ticks_msec() + 300000
external_poll_timer.start()
external_link_started.emit(external_provider)
elif operation == "status":
match str(data.get("status", "pending")):
"linked":
var linked_provider := external_provider
_reset_external_link()
external_linked.emit(linked_provider)
refresh()
"failed":
request_failed.emit(str(data.get("message", "The provider could not connect this account.")))
_reset_external_link()
func _poll_external_link() -> void:
if external_transaction_id.is_empty() or external_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
return
if Time.get_ticks_msec() >= external_poll_deadline_msec:
request_failed.emit("The account connection timed out.")
_reset_external_link()
return
external_operation = "status"
var error := external_request.request("%s/external/%s/status/%s" % [_auth_base(), external_provider, external_transaction_id])
if error != OK:
external_operation = ""
func _reset_external_link() -> void:
if external_poll_timer != null:
external_poll_timer.stop()
if OS.has_feature("web") and not external_provider.is_empty():
JavaScriptBridge.eval("window.localStorage.removeItem(%s);if(window.ranazeProfileExternalWindow&&!window.ranazeProfileExternalWindow.closed){window.ranazeProfileExternalWindow.close();}window.ranazeProfileExternalWindow=null;" % JSON.stringify(_external_storage_key()), true)
external_operation = ""
external_provider = ""
external_provider_name = ""
external_transaction_id = ""
external_poll_deadline_msec = 0
func _external_storage_key() -> String:
return "ranaze_profile_external_auth_url_%s" % external_provider
func _auth_base() -> String:
return EndpointConfig.resolve(str(ProjectSettings.get_setting("step3/auth_api_base_url", AUTH_URL)), "/api/Auth").rstrip("/")
func _save_archive(content: String) -> void:
var filename := "ranaze-account-export-%s.json" % Time.get_date_string_from_system()
if OS.has_feature("web"):
+78 -29
View File
@@ -3,7 +3,7 @@ extends Control
const Brand := preload("res://scripts/ui/medmind_style.gd")
const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
const AUTH_BASE := "https://us3q.ranaze.com/api/Auth"
const STEAM_AUTH_STORAGE_KEY := "ranaze_steam_auth_url"
const EXTERNAL_AUTH_STORAGE_PREFIX := "ranaze_external_auth_url_"
var main: Control
var auth_base_url := AUTH_BASE
@@ -38,6 +38,7 @@ var steam_username_input: LineEdit
var steam_email_input: LineEdit
var steam_password_input: LineEdit
var steam_legal_check: CheckBox
var external_buttons: Dictionary = {}
var panel_gutter: MarginContainer
var submit_buttons: Array[Button] = []
var fields: Array[LineEdit] = []
@@ -49,6 +50,9 @@ var steam_transaction_id := ""
var steam_poll_deadline_msec := 0
var steam_poll_failures := 0
var steam_popup_opened := false
var external_provider_id := ""
var external_provider_name := ""
var external_verified_email := ""
var automatic_login_pending := false
var login_attempt_username := ""
var compact_layout := false
@@ -63,6 +67,7 @@ func _ready() -> void:
_create_request("register", _on_register_completed)
_create_request("reset_request", _on_reset_requested)
_create_request("reset_confirm", _on_reset_confirmed)
_create_request("providers", _on_providers_loaded)
_create_request("steam_start", _on_steam_started)
_create_request("steam_status", _on_steam_status)
_create_request("steam_complete", _on_steam_completed)
@@ -76,6 +81,7 @@ func _ready() -> void:
_update_responsive_layout.call_deferred()
_set_mode("login")
_apply_theme()
_load_external_providers()
func on_show(payload: Variant = null) -> void:
@@ -249,12 +255,15 @@ func _build_login_view() -> VBoxContainer:
var sign_in := _add_primary_button(view, "Sign In", _on_login_pressed)
password_input.text_submitted.connect(func(_text: String): _on_login_pressed())
view.add_child(_build_divider())
var steam := Button.new()
steam.text = "Sign in with Steam"
steam.custom_minimum_size = Vector2(0, 68)
steam.pressed.connect(_start_steam)
view.add_child(steam)
submit_buttons.append(steam)
for provider in [["steam", "Steam"], ["google", "Google"]]:
var external := Button.new()
external.text = "Sign in with %s" % provider[1]
external.custom_minimum_size = Vector2(0, 68)
external.visible = false
external.pressed.connect(_start_external.bind(str(provider[0]), str(provider[1])))
view.add_child(external)
external_buttons[provider[0]] = external
submit_buttons.append(external)
var create := Button.new()
create.text = "New to RanaZe? Create an account"
create.flat = true
@@ -298,7 +307,7 @@ func _build_reset_view() -> VBoxContainer:
func _build_steam_view() -> VBoxContainer:
var view := _new_form()
var help := Label.new()
help.text = "Steam was verified. Create a RanaZe account or link an existing one."
help.text = "Your identity was verified. Create a RanaZe account or link an existing one."
help.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
view.add_child(help)
steam_action = OptionButton.new()
@@ -312,7 +321,7 @@ func _build_steam_view() -> VBoxContainer:
_add_show_password(view, [steam_password_input])
steam_legal_check = _add_legal_consent(view)
_add_primary_button(view, "Continue", _complete_steam)
_add_back_button(view, "Cancel Steam sign in", "login")
_add_back_button(view, "Cancel external sign in", "login")
return view
@@ -461,7 +470,7 @@ func _set_mode(next_mode: String) -> void:
match mode:
"register": heading.text = "Create your account"; subheading.text = "Start studying with RanaZe"
"reset": heading.text = "Reset your password"; subheading.text = "We will email you a verification code"
"steam": heading.text = "Finish Steam sign in"; subheading.text = "Connect Steam to RanaZe"
"steam": heading.text = "Finish %s sign in" % external_provider_name; subheading.text = "Connect %s to RanaZe" % external_provider_name
_: heading.text = "Welcome back"; subheading.text = "Sign in to continue"
_clear_message()
_update_responsive_layout()
@@ -538,7 +547,7 @@ func _apply_theme() -> void:
typed.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark))
typed.add_theme_stylebox_override("hover", Brand.secondary_button_box(dark, true))
for button in submit_buttons:
if button.text not in ["Sign in with Steam"]:
if not button.text.begins_with("Sign in with "):
button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
button.add_theme_color_override("font_color", Color.WHITE)
var theme_normal := StyleBoxFlat.new()
@@ -648,39 +657,66 @@ func _on_reset_confirmed(result: int, code: int, _headers: PackedStringArray, bo
_show_success("Password updated. You can now sign in.")
func _load_external_providers() -> void:
var request: HTTPRequest = requests["providers"]
if request.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED:
request.request("%s/external/providers" % auth_base_url)
func _on_providers_loaded(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
return
var data := _parse_dictionary(body)
var enabled: Dictionary = {}
for provider_value in data.get("providers", []):
if typeof(provider_value) == TYPE_DICTIONARY:
var provider: Dictionary = provider_value
enabled[str(provider.get("id", ""))] = true
for provider_id in external_buttons:
(external_buttons[provider_id] as Button).visible = enabled.has(provider_id)
func _start_steam() -> void:
_start_external("steam", "Steam")
func _start_external(provider_id: String, provider_name: String) -> void:
external_provider_id = provider_id
external_provider_name = provider_name
external_verified_email = ""
var storage_key := _external_storage_key()
if OS.has_feature("web"):
var popup_result: Variant = JavaScriptBridge.eval(
"window.localStorage.removeItem('%s'); var ranazeSteamBridge = new URL('/external-redirect.html?key=%s&provider=Steam', window.location.origin).href; window.ranazeSteamWindow = window.open(ranazeSteamBridge, 'ranaze_steam', 'popup=yes,width=560,height=720,resizable=yes,scrollbars=yes'); if (window.ranazeSteamWindow) { window.ranazeSteamWindow.blur(); window.focus(); } window.ranazeSteamWindow !== null;" % [STEAM_AUTH_STORAGE_KEY, STEAM_AUTH_STORAGE_KEY],
"window.localStorage.removeItem(%s); var ranazeExternalBridge = new URL('/external-redirect.html?key=' + encodeURIComponent(%s) + '&provider=' + encodeURIComponent(%s), window.location.origin).href; window.ranazeExternalWindow = window.open(ranazeExternalBridge, 'ranaze_external', 'popup=yes,width=560,height=720,resizable=yes,scrollbars=yes'); if (window.ranazeExternalWindow) { window.ranazeExternalWindow.blur(); window.focus(); } window.ranazeExternalWindow !== null;" % [JSON.stringify(storage_key), JSON.stringify(storage_key), JSON.stringify(provider_name)],
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...")
return _show_error("%s sign in needs a popup. Allow popups for this site, then try again." % provider_name)
_send_json("steam_start", "/external/%s/start" % provider_id, {"purpose": "login"}, "Opening %s..." % provider_name)
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.")
return _show_error("Unable to start %s sign in." % external_provider_name)
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():
_close_steam_popup()
return _show_error("Steam returned an invalid sign-in response.")
return _show_error("%s returned an invalid sign-in response." % external_provider_name)
if OS.has_feature("web"):
JavaScriptBridge.eval(
"var ranazeSteamAuthUrl = %s; window.localStorage.setItem('%s', ranazeSteamAuthUrl); if (window.ranazeSteamWindow && !window.ranazeSteamWindow.closed) { try { window.ranazeSteamWindow.location.replace(ranazeSteamAuthUrl); } catch (_) {} window.ranazeSteamWindow.focus(); } else { var ranazeSteamPopup = window.open(ranazeSteamAuthUrl, 'ranaze_steam'); if (ranazeSteamPopup) { window.ranazeSteamWindow = ranazeSteamPopup; ranazeSteamPopup.focus(); } else { window.location.assign(ranazeSteamAuthUrl); } }" % [JSON.stringify(auth_url), STEAM_AUTH_STORAGE_KEY],
"var ranazeExternalAuthUrl = %s; window.localStorage.setItem(%s, ranazeExternalAuthUrl); if (window.ranazeExternalWindow && !window.ranazeExternalWindow.closed) { try { window.ranazeExternalWindow.location.replace(ranazeExternalAuthUrl); } catch (_) {} window.ranazeExternalWindow.focus(); } else { var ranazeExternalPopup = window.open(ranazeExternalAuthUrl, 'ranaze_external'); if (ranazeExternalPopup) { window.ranazeExternalWindow = ranazeExternalPopup; ranazeExternalPopup.focus(); } else { window.location.assign(ranazeExternalAuthUrl); } }" % [JSON.stringify(auth_url), JSON.stringify(_external_storage_key())],
true
)
else:
OS.shell_open(auth_url)
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...")
_show_success("Complete sign in in the %s window. Waiting for verification..." % external_provider_name)
poll_timer.start()
@@ -690,28 +726,34 @@ func _poll_steam() -> void:
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])
var error: Error = status_request.request("%s/external/%s/status/%s" % [auth_base_url, external_provider_id, steam_transaction_id])
if error != OK:
steam_poll_failures += 1
if steam_poll_failures >= 3: _show_error("Connection interrupted. Still waiting for Steam...")
if steam_poll_failures >= 3: _show_error("Connection interrupted. Still waiting for %s..." % external_provider_name)
func _on_steam_status(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
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...")
if steam_poll_failures >= 3: _show_error("Connection interrupted. Still waiting for %s..." % external_provider_name)
return
if code == 404:
_reset_steam_flow()
return _show_error("Steam sign in expired. Please try again.")
return _show_error("%s sign in expired. Please try again." % external_provider_name)
if code < 200 or code >= 300:
_reset_steam_flow()
return _show_error(_response_message(body, "Steam sign in could not be completed."))
return _show_error(_response_message(body, "%s sign in could not be completed." % external_provider_name))
steam_poll_failures = 0
var data := _parse_dictionary(body)
match str(data.get("status", "pending")):
"pending": _show_success("Complete sign in in the Steam window. Waiting for verification...")
"requires_account": poll_timer.stop(); _close_steam_popup(); _set_mode("steam")
"pending": _show_success("Complete sign in in the %s window. Waiting for verification..." % external_provider_name)
"requires_account":
poll_timer.stop()
_close_steam_popup()
external_verified_email = str(data.get("email", ""))
steam_email_input.text = external_verified_email
steam_email_input.editable = external_verified_email.is_empty()
_set_mode("steam")
"complete": poll_timer.stop(); _close_steam_popup(); _accept_session(body, str(data.get("username", "")))
@@ -730,7 +772,7 @@ func _complete_steam() -> void:
return _show_error("Agree to the Terms of Service and acknowledge the Privacy Policy to create an account.")
elif steam_username_input.text.strip_edges().is_empty() or steam_password_input.text.is_empty():
return _show_error("Enter your existing account credentials.")
_send_json("steam_complete", "/steam/complete", {
_send_json("steam_complete", "/external/%s/complete" % external_provider_id, {
"transactionId": steam_transaction_id, "action": action,
"username": steam_username_input.text.strip_edges(), "email": steam_email_input.text.strip_edges(),
"password": steam_password_input.text, "acceptLegal": action == "create"
@@ -740,7 +782,7 @@ func _complete_steam() -> void:
func _on_steam_completed(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_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."))
return _show_error(_response_message(body, "Unable to connect this %s account." % external_provider_name))
steam_poll_failures = 0
poll_timer.start()
_poll_steam()
@@ -748,18 +790,25 @@ func _on_steam_completed(result: int, code: int, _headers: PackedStringArray, bo
func _reset_steam_flow() -> void:
if poll_timer != null: poll_timer.stop()
_close_steam_popup()
steam_transaction_id = ""
steam_poll_deadline_msec = 0
steam_poll_failures = 0
_close_steam_popup()
external_provider_id = ""
external_provider_name = ""
external_verified_email = ""
func _close_steam_popup() -> void:
if OS.has_feature("web") and steam_popup_opened:
JavaScriptBridge.eval("window.localStorage.removeItem('%s'); if (window.ranazeSteamWindow && !window.ranazeSteamWindow.closed) { window.ranazeSteamWindow.close(); } window.ranazeSteamWindow = null;" % STEAM_AUTH_STORAGE_KEY, true)
JavaScriptBridge.eval("window.localStorage.removeItem(%s); if (window.ranazeExternalWindow && !window.ranazeExternalWindow.closed) { window.ranazeExternalWindow.close(); } window.ranazeExternalWindow = null;" % JSON.stringify(_external_storage_key()), true)
steam_popup_opened = false
func _external_storage_key() -> String:
return EXTERNAL_AUTH_STORAGE_PREFIX + external_provider_id
func _accept_session(body: PackedByteArray, fallback_username: String) -> void:
var response := _parse_dictionary(body)
var token := str(response.get("accessToken", ""))
+47
View File
@@ -18,6 +18,7 @@ var outcome_date: LineEdit
var outcome_consent: CheckBox
var outcome_status: Label
var outcome_delete: Button
var connected_accounts_list: VBoxContainer
func _ready() -> void:
@@ -26,6 +27,8 @@ func _ready() -> void:
AccountProfileService.request_failed.connect(_show_error)
AccountProfileService.preferences_saved.connect(func(): _show_saved("Study identity saved."))
AccountProfileService.archive_saved.connect(func(path: String): _show_saved("Archive saved: %s" % path))
AccountProfileService.external_link_started.connect(func(provider: String): _show_saved("Complete %s connection in the opened window." % provider.capitalize()))
AccountProfileService.external_linked.connect(func(provider: String): _show_saved("%s account connected." % provider.capitalize()))
DashboardService.dashboard_changed.connect(_render)
DashboardService.dashboard_failed.connect(_show_error)
ExamSimulationService.outcome_changed.connect(_render_outcome)
@@ -82,6 +85,7 @@ func _build() -> void:
right.add_theme_constant_override("separation", 14)
columns.add_child(right)
right.add_child(_outcome_card())
right.add_child(_connected_accounts_card())
right.add_child(_account_actions_card())
_update_responsive()
_render()
@@ -228,6 +232,16 @@ func _account_actions_card() -> PanelContainer:
return _card(body)
func _connected_accounts_card() -> PanelContainer:
var body := VBoxContainer.new()
body.add_theme_constant_override("separation", 9)
body.add_child(_section_header("Connected Accounts", "Use another trusted identity to sign in"))
connected_accounts_list = VBoxContainer.new()
connected_accounts_list.add_theme_constant_override("separation", 8)
body.add_child(connected_accounts_list)
return _card(body)
func _render() -> void:
if profile_name == null:
return
@@ -241,6 +255,7 @@ func _render() -> void:
var role := str(account.get("role", AuthState.role)).capitalize()
var created := _date_only(str(account.get("createdUtc", "")))
profile_meta.text = role if created.is_empty() else "%s | Member since %s" % [role, created]
_render_connected_accounts(account.get("connectedProviders", []), account.get("availableProviders", []))
(identity_values["exam"] as Label).text = _date_only(str(prefs.get("targetExamDate", "")), "Not scheduled")
(identity_values["stage"] as Label).text = _friendly(str(prefs.get("preparationStage", "pre_dedicated")))
(identity_values["goal"] as Label).text = "%d hours / week" % int(prefs.get("weeklyGoalHours", 10))
@@ -256,6 +271,38 @@ func _render() -> void:
_render_outcome()
func _render_connected_accounts(provider_values: Variant, available_values: Variant) -> void:
if connected_accounts_list == null:
return
_clear(connected_accounts_list)
var connected: Dictionary = {}
var available: Dictionary = {}
if typeof(provider_values) == TYPE_ARRAY:
for value in provider_values:
connected[str(value).to_lower()] = true
if typeof(available_values) == TYPE_ARRAY:
for value in available_values:
available[str(value).to_lower()] = true
for provider in [["steam", "Steam"], ["google", "Google"]]:
var provider_id := str(provider[0])
var provider_name := str(provider[1])
if not connected.has(provider_id) and not available.has(provider_id):
continue
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
var copy := VBoxContainer.new()
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
copy.add_child(_label(provider_name, 14, Brand.text_color(_dark()), true))
copy.add_child(_label("Connected" if connected.has(provider_id) else "Not connected", 12,
Brand.SUCCESS if connected.has(provider_id) else Brand.muted_color(_dark())))
row.add_child(copy)
if not connected.has(provider_id):
var connect := _button("Connect", "link", false)
connect.pressed.connect(AccountProfileService.connect_provider.bind(provider_id, provider_name))
row.add_child(connect)
connected_accounts_list.add_child(row)
func _render_systems(systems: Array = DashboardService.data.get("systems", [])) -> void:
if systems_list == null:
return