Add session expiry renewal flow

This commit is contained in:
hz
2026-07-18 15:53:11 -05:00
parent 8c349314ad
commit a232e18c46
14 changed files with 575 additions and 21 deletions
+5 -1
View File
@@ -72,10 +72,14 @@ func _ready() -> void:
_apply_theme()
func on_show(_payload: Variant = null) -> void:
func on_show(payload: Variant = null) -> void:
_set_busy(false)
_set_mode("login")
_clear_message()
var login_message := str(payload).strip_edges() if payload != null else ""
if not login_message.is_empty():
message_label.text = login_message
message_label.add_theme_color_override("font_color", Color("ef4444"))
login_input.grab_focus()
+19 -2
View File
@@ -12,6 +12,7 @@ var sound_toggle: CheckButton
var theme_button: Button
var feedback_button: Button
var feedback_dialog: Control
var session_expiry_dialog: Control
var theme_service
var current_screen_name := ""
var screens_margin: MarginContainer
@@ -285,10 +286,15 @@ func _build_ui() -> void:
feedback_dialog = preload("res://scripts/ui/app_feedback_dialog.gd").new()
feedback_dialog.name = "AppFeedbackDialog"
add_child(feedback_dialog)
session_expiry_dialog = preload("res://scripts/ui/session_expiry_dialog.gd").new()
session_expiry_dialog.name = "SessionExpiryDialog"
session_expiry_dialog.logout_requested.connect(_logout)
add_child(session_expiry_dialog)
_build_account_menu()
_build_drawer()
AuthState.auth_changed.connect(_refresh_header_controls)
AuthState.session_expired.connect(_on_session_expired)
_refresh_header_controls()
_build_app_menu()
@@ -682,14 +688,25 @@ func _refresh_theme_button() -> void:
func _logout() -> void:
_perform_logout("")
func _on_session_expired(_reason: String) -> void:
_perform_logout("Your session expired. Sign in again.")
func _perform_logout(login_message: String) -> void:
_close_app_menu()
if session_expiry_dialog != null and session_expiry_dialog.has_method("dismiss_for_logout"):
session_expiry_dialog.dismiss_for_logout()
if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation":
BlockSessionService.finish_block()
elif BlockSessionService.session_kind == "simulation":
BlockSessionService.block_started = false
QuestionBankService.clear_questions()
AuthState.clear_session()
show_screen("login")
if AuthState.is_logged_in or not AuthState.access_token.is_empty():
AuthState.clear_session()
show_screen("login", login_message)
func _build_app_menu() -> void:
+11 -1
View File
@@ -652,6 +652,10 @@ func _toolbar_box(dark: bool, active: bool = false) -> StyleBoxFlat:
box.border_color = Brand.PURPLE if active else (Color("33415d") if dark else Brand.LIGHT_BORDER)
box.set_border_width_all(1)
box.set_corner_radius_all(7)
box.content_margin_left = 0
box.content_margin_right = 0
box.content_margin_top = 0
box.content_margin_bottom = 0
return box
@@ -687,7 +691,13 @@ func _style_asset_toolbar_button(button: Button, texture: Texture2D, dark: bool)
button.icon = texture
button.expand_icon = false
button.focus_mode = Control.FOCUS_NONE
button.add_theme_constant_override("icon_max_width", 25)
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
button.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER
button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
button.add_theme_constant_override("h_separation", 0)
button.add_theme_constant_override("icon_max_width", 23)
button.add_theme_stylebox_override("normal", _toolbar_box(dark))
button.add_theme_stylebox_override("hover", _toolbar_box(dark, true))
button.add_theme_stylebox_override("pressed", _toolbar_box(dark, true))
+181
View File
@@ -0,0 +1,181 @@
extends Control
const Brand := preload("res://scripts/ui/medmind_style.gd")
signal logout_requested
var panel: PanelContainer
var title_label: Label
var message_label: Label
var error_label: Label
var stay_button: Button
var logout_button: Button
func _ready() -> void:
visible = false
top_level = true
mouse_filter = Control.MOUSE_FILTER_STOP
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_build_ui()
AuthState.session_warning.connect(_show_warning)
AuthState.session_countdown_changed.connect(_update_countdown)
AuthState.session_refresh_started.connect(_refresh_started)
AuthState.session_refreshed.connect(_refresh_succeeded)
AuthState.session_refresh_failed.connect(_refresh_failed)
UIThemeService.theme_changed.connect(func(_dark: bool): _apply_theme())
resized.connect(_refresh_size)
_refresh_size()
_apply_theme()
func _build_ui() -> void:
var dim := ColorRect.new()
dim.color = Color(0, 0, 0, 0.74)
dim.mouse_filter = Control.MOUSE_FILTER_STOP
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(dim)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(center)
panel = PanelContainer.new()
panel.custom_minimum_size = Vector2(520, 280)
center.add_child(panel)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 16)
panel.add_child(content)
title_label = Label.new()
title_label.text = "Stay logged in?"
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_label.add_theme_font_size_override("font_size", 28)
content.add_child(title_label)
message_label = Label.new()
message_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
message_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
message_label.add_theme_font_size_override("font_size", 17)
content.add_child(message_label)
error_label = Label.new()
error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
error_label.add_theme_font_size_override("font_size", 14)
content.add_child(error_label)
var actions := HBoxContainer.new()
actions.add_theme_constant_override("separation", 12)
content.add_child(actions)
logout_button = Button.new()
logout_button.text = "Log out"
logout_button.custom_minimum_size = Vector2(150, 48)
logout_button.pressed.connect(_request_logout)
actions.add_child(logout_button)
stay_button = Button.new()
stay_button.text = "Stay logged in"
stay_button.custom_minimum_size = Vector2(0, 48)
stay_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
stay_button.pressed.connect(AuthState.request_session_refresh)
actions.add_child(stay_button)
func _show_warning(seconds_remaining: int) -> void:
error_label.text = ""
stay_button.disabled = false
logout_button.disabled = false
stay_button.text = "Stay logged in"
visible = true
_update_countdown(seconds_remaining)
move_to_front()
BlockSessionService.pause_current_question_timer()
func _update_countdown(seconds_remaining: int) -> void:
if not visible:
return
var minutes := floori(float(seconds_remaining) / 60.0)
var seconds := seconds_remaining % 60
message_label.text = "Your session expires in %02d:%02d. Renew it to continue studying." % [minutes, seconds]
func _refresh_started() -> void:
if not visible:
return
stay_button.disabled = true
logout_button.disabled = true
stay_button.text = "Renewing..."
error_label.text = ""
func _refresh_succeeded() -> void:
_hide_dialog()
func _refresh_failed(message: String, terminal: bool) -> void:
if not visible:
return
error_label.text = message
stay_button.text = "Stay logged in"
stay_button.disabled = terminal
logout_button.disabled = false
func _request_logout() -> void:
logout_requested.emit()
func _hide_dialog() -> void:
visible = false
error_label.text = ""
stay_button.text = "Stay logged in"
stay_button.disabled = false
logout_button.disabled = false
BlockSessionService.resume_current_question_timer()
func dismiss_for_logout() -> void:
if visible:
_hide_dialog()
func _unhandled_input(event: InputEvent) -> void:
if visible and event.is_action_pressed("ui_cancel"):
get_viewport().set_input_as_handled()
func _refresh_size() -> void:
if panel == null:
return
panel.custom_minimum_size = Vector2(
minf(520.0, maxf(288.0, size.x - 32.0)),
minf(280.0, maxf(250.0, size.y - 32.0)))
func _apply_theme() -> void:
if panel == null:
return
var dark := UIThemeService.is_dark_mode()
var panel_style := Brand.panel_box(dark)
panel_style.content_margin_left = 32
panel_style.content_margin_right = 32
panel_style.content_margin_top = 30
panel_style.content_margin_bottom = 30
panel.add_theme_stylebox_override("panel", panel_style)
for label in [title_label, message_label]:
label.add_theme_color_override("font_color", Brand.text_color(dark))
error_label.add_theme_color_override("font_color", Color("fb7185") if dark else Color("be123c"))
stay_button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
stay_button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
stay_button.add_theme_stylebox_override("pressed", Brand.gradient_button_box())
stay_button.add_theme_color_override("font_color", Color.WHITE)
stay_button.add_theme_color_override("font_hover_color", Color.WHITE)
logout_button.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark))
logout_button.add_theme_stylebox_override("hover", Brand.secondary_button_box(dark, true))
logout_button.add_theme_stylebox_override("pressed", Brand.secondary_button_box(dark, true))
logout_button.add_theme_color_override("font_color", Brand.text_color(dark))
logout_button.add_theme_color_override("font_hover_color", Brand.text_color(dark))
+1
View File
@@ -0,0 +1 @@
uid://dv8ssk14s6suf