Files
step3/scripts/ui/donation_screen.gd
T

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()