335 lines
13 KiB
GDScript
335 lines
13 KiB
GDScript
extends MarginContainer
|
|
|
|
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
|
const Style := preload("res://scripts/ui/ui_style.gd")
|
|
const DonationAmount := preload("res://scripts/ui/donation_amount.gd")
|
|
const MINIMUM_CENTS := 500
|
|
const MAXIMUM_CENTS := 1000000
|
|
const DEFAULT_CENTS := 500
|
|
|
|
var main: Control
|
|
var content_grid: GridContainer
|
|
var mission_panel: PanelContainer
|
|
var contribution_panel: PanelContainer
|
|
var preset_grid: GridContainer
|
|
var amount_buttons: Array[Button] = []
|
|
var muted_labels: Array[Label] = []
|
|
var amount_group := ButtonGroup.new()
|
|
var custom_amount: LineEdit
|
|
var donate_button: Button
|
|
var status_label: Label
|
|
var selected_amount_cents := DEFAULT_CENTS
|
|
var donation_service: Node
|
|
|
|
|
|
func _ready() -> void:
|
|
donation_service = get_node("/root/DonationService")
|
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
add_theme_constant_override("margin_bottom", 8)
|
|
_build_ui()
|
|
resized.connect(_update_layout)
|
|
donation_service.checkout_started.connect(_on_checkout_started)
|
|
donation_service.donation_status_changed.connect(_on_donation_status_changed)
|
|
donation_service.donation_failed.connect(_on_donation_failed)
|
|
UIThemeService.theme_changed.connect(_on_theme_changed)
|
|
_update_layout()
|
|
_update_donate_button()
|
|
refresh_theme_controls()
|
|
|
|
|
|
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.custom_minimum_size = Vector2(0, 0)
|
|
page.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
page.add_theme_constant_override("separation", 24)
|
|
scroll.add_child(page)
|
|
|
|
var title := Label.new()
|
|
title.text = "Support RanaZe"
|
|
title.add_theme_font_override("font", Brand.font)
|
|
title.add_theme_font_size_override("font_size", 36)
|
|
page.add_child(title)
|
|
|
|
var subtitle := Label.new()
|
|
subtitle.text = "Help us build accessible, clinically grounded tools for Step 3 learners."
|
|
subtitle.add_theme_font_override("font", Brand.font)
|
|
subtitle.add_theme_font_size_override("font_size", 18)
|
|
subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
|
|
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
muted_labels.append(subtitle)
|
|
page.add_child(subtitle)
|
|
|
|
content_grid = GridContainer.new()
|
|
content_grid.columns = 2
|
|
content_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
content_grid.add_theme_constant_override("h_separation", 24)
|
|
content_grid.add_theme_constant_override("v_separation", 24)
|
|
page.add_child(content_grid)
|
|
|
|
mission_panel = _panel()
|
|
mission_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
content_grid.add_child(mission_panel)
|
|
var mission := VBoxContainer.new()
|
|
mission.add_theme_constant_override("separation", 18)
|
|
mission_panel.add_child(mission)
|
|
mission.add_child(_heading("Our mission", 26))
|
|
mission.add_child(_body("RanaZe exists to make rigorous, clinically grounded medical education more accessible. We are building an independent Step 3 study experience that helps learners develop clinical judgment through original questions, realistic CCS cases, transparent explanations, and continuous improvement driven by learner feedback."))
|
|
mission.add_child(HSeparator.new())
|
|
mission.add_child(_heading("What your support makes possible", 20))
|
|
for item_text in [
|
|
"Original question and CCS case development",
|
|
"Clinical source review and content verification",
|
|
"Reliable hosting and continued pre-alpha access",
|
|
"Accessibility, usability, and learning-tool improvements"
|
|
]:
|
|
var row := HBoxContainer.new()
|
|
row.add_theme_constant_override("separation", 12)
|
|
var marker := Label.new()
|
|
marker.text = "-"
|
|
marker.add_theme_font_size_override("font_size", 20)
|
|
marker.add_theme_color_override("font_color", Brand.TEAL)
|
|
row.add_child(marker)
|
|
var item := _body(item_text)
|
|
item.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
row.add_child(item)
|
|
mission.add_child(row)
|
|
|
|
contribution_panel = _panel()
|
|
contribution_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
content_grid.add_child(contribution_panel)
|
|
var contribution := VBoxContainer.new()
|
|
contribution.add_theme_constant_override("separation", 18)
|
|
contribution_panel.add_child(contribution)
|
|
contribution.add_child(_heading("Make a one-time contribution", 26))
|
|
contribution.add_child(_body("Choose an amount. Secure payment and email receipts are handled by Stripe."))
|
|
|
|
preset_grid = GridContainer.new()
|
|
preset_grid.columns = 4
|
|
preset_grid.add_theme_constant_override("h_separation", 10)
|
|
preset_grid.add_theme_constant_override("v_separation", 10)
|
|
contribution.add_child(preset_grid)
|
|
for amount in [500, 1000, 2500, 5000]:
|
|
var button := Button.new()
|
|
button.text = "$%d" % int(amount / 100.0)
|
|
button.toggle_mode = true
|
|
button.button_group = amount_group
|
|
button.custom_minimum_size = Vector2(64, 48)
|
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
button.set_meta("amount_cents", amount)
|
|
button.pressed.connect(_select_preset.bind(button))
|
|
preset_grid.add_child(button)
|
|
amount_buttons.append(button)
|
|
if amount == DEFAULT_CENTS:
|
|
button.set_pressed_no_signal(true)
|
|
|
|
var custom_label := Label.new()
|
|
custom_label.text = "Custom amount (USD)"
|
|
custom_label.add_theme_font_override("font", Brand.font)
|
|
custom_label.add_theme_font_size_override("font_size", 16)
|
|
contribution.add_child(custom_label)
|
|
|
|
custom_amount = LineEdit.new()
|
|
custom_amount.placeholder_text = "$5.00 minimum"
|
|
custom_amount.max_length = 12
|
|
custom_amount.virtual_keyboard_type = LineEdit.KEYBOARD_TYPE_NUMBER_DECIMAL
|
|
custom_amount.custom_minimum_size = Vector2(0, 56)
|
|
custom_amount.text_changed.connect(_on_custom_amount_changed)
|
|
contribution.add_child(custom_amount)
|
|
|
|
donate_button = Button.new()
|
|
donate_button.text = "Donate $5 securely"
|
|
donate_button.custom_minimum_size = Vector2(0, 58)
|
|
donate_button.pressed.connect(_on_donate_pressed)
|
|
contribution.add_child(donate_button)
|
|
|
|
status_label = Label.new()
|
|
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
status_label.add_theme_font_override("font", Brand.font)
|
|
status_label.add_theme_font_size_override("font_size", 15)
|
|
status_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
|
|
status_label.custom_minimum_size.y = 44
|
|
contribution.add_child(status_label)
|
|
|
|
var disclosure := _body("Contributions are voluntary, one-time, and not tax-deductible. Card details are entered only on Stripe's secure checkout page.")
|
|
disclosure.add_theme_font_size_override("font_size", 14)
|
|
contribution.add_child(disclosure)
|
|
var legal_links := HBoxContainer.new()
|
|
legal_links.add_theme_constant_override("separation", 12)
|
|
contribution.add_child(legal_links)
|
|
for spec in [["Terms of Service", "terms"], ["Privacy Policy", "privacy"]]:
|
|
var link := Button.new()
|
|
link.text = spec[0]
|
|
link.flat = true
|
|
link.pressed.connect(_open_legal_document.bind(str(spec[1])))
|
|
legal_links.add_child(link)
|
|
|
|
|
|
func on_show(_payload: Variant = null) -> void:
|
|
donate_button.disabled = donation_service.checkout_pending
|
|
if donation_service.active_donation_id.is_empty():
|
|
status_label.text = ""
|
|
else:
|
|
donation_service.resume_polling()
|
|
|
|
|
|
func on_hide() -> void:
|
|
donation_service.stop_polling()
|
|
|
|
|
|
func _open_legal_document(kind: String) -> void:
|
|
main.open_legal_document(kind)
|
|
|
|
|
|
static func parse_amount_cents(value: String) -> int:
|
|
return DonationAmount.parse_cents(value)
|
|
|
|
|
|
func _select_preset(button: Button) -> void:
|
|
selected_amount_cents = int(button.get_meta("amount_cents", DEFAULT_CENTS))
|
|
custom_amount.text = ""
|
|
_set_validation_state("")
|
|
_update_donate_button()
|
|
refresh_theme_controls()
|
|
|
|
|
|
func _on_custom_amount_changed(value: String) -> void:
|
|
if value.strip_edges().is_empty():
|
|
return
|
|
for button in amount_buttons:
|
|
button.set_pressed_no_signal(false)
|
|
selected_amount_cents = parse_amount_cents(value)
|
|
_set_validation_state("")
|
|
_update_donate_button()
|
|
refresh_theme_controls()
|
|
|
|
|
|
func _on_donate_pressed() -> void:
|
|
if selected_amount_cents < MINIMUM_CENTS:
|
|
_set_validation_state("Enter an amount of at least $5.00.", true)
|
|
return
|
|
if selected_amount_cents > MAXIMUM_CENTS:
|
|
_set_validation_state("The maximum contribution is $10,000.00.", true)
|
|
return
|
|
donate_button.disabled = true
|
|
_set_validation_state("Opening secure checkout...")
|
|
donation_service.begin_checkout(selected_amount_cents)
|
|
|
|
|
|
func _on_checkout_started(_donation_id: String, _checkout_url: String) -> void:
|
|
donate_button.disabled = false
|
|
_set_validation_state("Checkout opened in your browser. This page will confirm when Stripe completes the payment.")
|
|
|
|
|
|
func _on_donation_status_changed(status: String, _donation: Dictionary) -> void:
|
|
match status:
|
|
"paid": _set_validation_state("Thank you for supporting RanaZe. Your contribution has been confirmed.", false, Brand.SUCCESS)
|
|
"failed": _set_validation_state("Stripe could not complete the contribution. No payment was confirmed.", true)
|
|
"expired": _set_validation_state("The checkout session expired. Start a new checkout when you are ready.", true)
|
|
|
|
|
|
func _on_donation_failed(message: String) -> void:
|
|
donate_button.disabled = false
|
|
_set_validation_state(message, true)
|
|
|
|
|
|
func _update_donate_button() -> void:
|
|
donate_button.disabled = selected_amount_cents < MINIMUM_CENTS or selected_amount_cents > MAXIMUM_CENTS
|
|
if selected_amount_cents >= MINIMUM_CENTS and selected_amount_cents <= MAXIMUM_CENTS:
|
|
donate_button.text = "Donate %s securely" % _format_amount(selected_amount_cents)
|
|
else:
|
|
donate_button.text = "Donate securely"
|
|
|
|
|
|
func _format_amount(amount_cents: int) -> String:
|
|
return "$%d.%02d" % [int(amount_cents / 100.0), amount_cents % 100]
|
|
|
|
|
|
func _set_validation_state(message: String, is_error: bool = false, color: Color = Color.TRANSPARENT) -> void:
|
|
status_label.text = message
|
|
var text_color := color
|
|
if text_color == Color.TRANSPARENT:
|
|
text_color = Style.RED if is_error else Brand.muted_color(UIThemeService.is_dark_mode())
|
|
status_label.add_theme_color_override("font_color", text_color)
|
|
|
|
|
|
func _panel() -> PanelContainer:
|
|
var panel := PanelContainer.new()
|
|
var box := Brand.content_card_box(UIThemeService.is_dark_mode(), false)
|
|
box.content_margin_left = 28
|
|
box.content_margin_right = 28
|
|
box.content_margin_top = 28
|
|
box.content_margin_bottom = 28
|
|
panel.add_theme_stylebox_override("panel", box)
|
|
return panel
|
|
|
|
|
|
func _heading(value: String, font_size: int) -> Label:
|
|
var label := Label.new()
|
|
label.text = value
|
|
label.add_theme_font_override("font", Brand.font)
|
|
label.add_theme_font_size_override("font_size", font_size)
|
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
return label
|
|
|
|
|
|
func _body(value: String) -> Label:
|
|
var label := Label.new()
|
|
label.text = value
|
|
label.add_theme_font_override("font", Brand.font)
|
|
label.add_theme_font_size_override("font_size", 17)
|
|
label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
|
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
muted_labels.append(label)
|
|
return label
|
|
|
|
|
|
func _update_layout() -> void:
|
|
if content_grid != null:
|
|
content_grid.columns = 1 if size.x < 1050.0 else 2
|
|
if preset_grid != null:
|
|
preset_grid.columns = 2 if size.x < 520.0 else 4
|
|
|
|
|
|
func refresh_theme_controls() -> void:
|
|
var dark := UIThemeService.is_dark_mode()
|
|
for label in muted_labels:
|
|
label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
|
for panel in [mission_panel, contribution_panel]:
|
|
if panel != null:
|
|
var box := Brand.content_card_box(dark, false)
|
|
box.content_margin_left = 28
|
|
box.content_margin_right = 28
|
|
box.content_margin_top = 28
|
|
box.content_margin_bottom = 28
|
|
panel.add_theme_stylebox_override("panel", box)
|
|
if custom_amount != null:
|
|
custom_amount.add_theme_stylebox_override("normal", Brand.field_box(dark))
|
|
custom_amount.add_theme_stylebox_override("focus", Brand.field_box(dark, true))
|
|
custom_amount.add_theme_color_override("font_color", Brand.text_color(dark))
|
|
custom_amount.add_theme_color_override("font_placeholder_color", Brand.muted_color(dark))
|
|
for button in amount_buttons:
|
|
var selected := button.button_pressed
|
|
var normal := Brand.secondary_button_box(dark, selected)
|
|
if selected:
|
|
normal.bg_color = Color(Brand.ROYAL_BLUE, 0.22 if dark else 0.10)
|
|
normal.border_color = Brand.ROYAL_BLUE
|
|
button.add_theme_stylebox_override("normal", normal)
|
|
button.add_theme_stylebox_override("pressed", normal)
|
|
button.add_theme_color_override("font_color", Brand.text_color(dark))
|
|
button.add_theme_color_override("font_pressed_color", Brand.text_color(dark))
|
|
if donate_button != null:
|
|
donate_button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
|
|
donate_button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
|
|
donate_button.add_theme_color_override("font_color", Color.WHITE)
|
|
donate_button.add_theme_color_override("font_hover_color", Color.WHITE)
|
|
|
|
|
|
func _on_theme_changed(_is_dark_mode: bool) -> void:
|
|
refresh_theme_controls()
|