extends MarginContainer const Brand := preload("res://scripts/ui/medmind_style.gd") var main: Control var summary: Label var quota: Label var status_label: Label var token_labels: Dictionary = {} var action_buttons: Array[Button] = [] var reset_dialog: ConfirmationDialog func _ready() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _build_ui() AccessService.status_changed.connect(_render) AccessService.operation_succeeded.connect(_on_success) AccessService.operation_failed.connect(_on_failure) AccessService.checkout_started.connect(_on_checkout_started) UIThemeService.theme_changed.connect(refresh_theme_controls) AccessService.refresh() func on_show(_payload: Variant = null) -> void: AccessService.refresh() _render(AccessService.status) func _build_ui() -> void: var scroll := ScrollContainer.new() scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED add_child(scroll) var page := VBoxContainer.new() page.size_flags_horizontal = Control.SIZE_EXPAND_FILL page.add_theme_constant_override("separation", 20) scroll.add_child(page) var title := Label.new() title.text = "Credits & Access" title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 36) page.add_child(title) var intro := Label.new() intro.text = "Study 20 QBank questions in a rolling 24-hour window. Eligible first attempts earn credits; 100 credits unlocks unlimited QBank access for 24 hours." intro.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART intro.add_theme_font_override("font", Brand.font) intro.add_theme_font_size_override("font_size", 17) page.add_child(intro) var overview := _panel() page.add_child(overview) var overview_content := VBoxContainer.new() overview_content.add_theme_constant_override("separation", 10) overview.add_child(overview_content) summary = _label("", 24) quota = _label("", 17) overview_content.add_child(summary) overview_content.add_child(quota) var redeem := _button("Redeem 100 credits for 24 hours", AccessService.redeem_credits) overview_content.add_child(redeem) var token_grid := GridContainer.new() token_grid.columns = 3 token_grid.add_theme_constant_override("h_separation", 16) token_grid.add_theme_constant_override("v_separation", 16) page.add_child(token_grid) _add_access_card(token_grid, "day", "1 day", "$1.00", "24 hours") _add_access_card(token_grid, "week", "1 week", "$5.00", "168 hours") _add_access_card(token_grid, "month", "1 month", "$20.00", "30 days") var extras := _panel() page.add_child(extras) var extras_content := VBoxContainer.new() extras_content.add_theme_constant_override("separation", 12) extras.add_child(extras_content) extras_content.add_child(_label("Study tools", 24)) token_labels["exam"] = _label("", 16) extras_content.add_child(token_labels["exam"]) extras_content.add_child(_button("Buy exam simulation token — $1.99", func(): AccessService.begin_checkout("exam_token_1"))) token_labels["reset"] = _label("", 16) extras_content.add_child(token_labels["reset"]) extras_content.add_child(_button("Buy QBank reset token — $4.99", func(): AccessService.begin_checkout("qbank_reset_1"))) extras_content.add_child(_button("Use QBank reset token", _confirm_reset)) status_label = _label("", 15) status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART page.add_child(status_label) reset_dialog = ConfirmationDialog.new() reset_dialog.title = "Reset QBank progress?" reset_dialog.dialog_text = "This starts a new QBank cycle. Questions become unused and credit-eligible again. Historical aggregate samples, bookmarks, notes, and review content remain." reset_dialog.ok_button_text = "Reset QBank" reset_dialog.confirmed.connect(AccessService.reset_qbank) add_child(reset_dialog) refresh_theme_controls() func _add_access_card(parent: GridContainer, token: String, heading: String, price: String, duration: String) -> void: var panel := _panel() panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL parent.add_child(panel) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 10) panel.add_child(content) content.add_child(_label(heading, 24)) content.add_child(_label("%s of unlimited QBank access" % duration, 15)) token_labels[token] = _label("", 15) content.add_child(token_labels[token]) content.add_child(_button("Buy token — %s" % price, func(): AccessService.begin_checkout("access_%s_1" % token))) content.add_child(_button("Activate token", func(): AccessService.activate_token(token))) func _render(value: Dictionary) -> void: if summary == null: return var credits := int(value.get("credits", 0)) var unlimited := bool(value.get("unlimited", false)) var expiry := str(value.get("unlimitedUntilUtc", "")) summary.text = "%d credits • %s" % [ credits, ("Unlimited QBank active until %s" % expiry) if unlimited else "Free QBank access" ] quota.text = "%d of %d free questions remaining in the rolling window" % [ int(value.get("questionsRemaining", 20)), int(value.get("freeQuestionLimit", 20)) ] token_labels.get("day", _label()).text = "%d available" % int(value.get("dayTokens", 0)) token_labels.get("week", _label()).text = "%d available" % int(value.get("weekTokens", 0)) token_labels.get("month", _label()).text = "%d available" % int(value.get("monthTokens", 0)) token_labels.get("exam", _label()).text = "Exam tokens: %d" % int(value.get("examTokens", 0)) token_labels.get("reset", _label()).text = "QBank reset tokens: %d" % int(value.get("resetTokens", 0)) func _confirm_reset() -> void: reset_dialog.popup_centered(Vector2i(620, 260)) func _on_success(message: String) -> void: status_label.text = message status_label.add_theme_color_override("font_color", Brand.SUCCESS) func _on_failure(message: String) -> void: status_label.text = message status_label.add_theme_color_override("font_color", Color("ef4444")) func _on_checkout_started(_purchase_id: String, _url: String) -> void: status_label.text = "Complete checkout in the secure payment window." func refresh_theme_controls() -> void: for button in action_buttons: var dark := UIThemeService.is_dark_mode() button.add_theme_font_override("font", Brand.font) button.add_theme_color_override("font_color", Brand.text_color(dark)) button.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark)) button.add_theme_stylebox_override("hover", Brand.secondary_button_box(dark, true)) button.add_theme_stylebox_override("pressed", Brand.secondary_button_box(dark, true)) func _panel() -> PanelContainer: var panel := PanelContainer.new() panel.add_theme_stylebox_override("panel", Brand.panel_box(UIThemeService.is_dark_mode())) return panel func _label(text := "", size := 16) -> Label: var label := Label.new() label.text = text label.add_theme_font_override("font", Brand.font) label.add_theme_font_size_override("font_size", size) label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART return label func _button(text: String, callback: Callable) -> Button: var button := Button.new() button.text = text button.custom_minimum_size = Vector2(0, 48) button.pressed.connect(callback) action_buttons.append(button) return button