extends Node const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd") const DEFAULT_URL := "https://s3q.ranaze.com/api/donations" const REQUEST_TIMEOUT_SECONDS := 30.0 const POLL_INTERVAL_SECONDS := 3.0 const POLL_LIMIT := 200 const CHECKOUT_STORAGE_KEY := "medmindDonationCheckoutUrl" signal checkout_started(donation_id: String, checkout_url: String) signal donation_status_changed(status: String, donation: Dictionary) signal donation_failed(message: String) var api_url := DEFAULT_URL var checkout_request: HTTPRequest var status_request: HTTPRequest var poll_timer: Timer var request_token := "" var active_donation_id := "" var poll_count := 0 var checkout_pending := false var status_pending := false func _ready() -> void: api_url = EndpointConfig.resolve( str(ProjectSettings.get_setting("step3/donations_api_url", DEFAULT_URL)), "/api/donations") checkout_request = HTTPRequest.new() checkout_request.timeout = REQUEST_TIMEOUT_SECONDS checkout_request.request_completed.connect(_on_checkout_completed) add_child(checkout_request) status_request = HTTPRequest.new() status_request.timeout = REQUEST_TIMEOUT_SECONDS status_request.request_completed.connect(_on_status_completed) add_child(status_request) poll_timer = Timer.new() poll_timer.wait_time = POLL_INTERVAL_SECONDS poll_timer.timeout.connect(_poll_status) add_child(poll_timer) AuthState.auth_changed.connect(_on_auth_changed) func begin_checkout(amount_cents: int) -> void: if checkout_pending: return if not AuthState.can_send_authenticated_request(): donation_failed.emit("Log in before making a contribution.") return _prepare_browser_window() checkout_pending = true request_token = AuthState.access_token var headers := PackedStringArray([ "Content-Type: application/json", "Authorization: Bearer %s" % request_token ]) var error := checkout_request.request( "%s/checkout" % api_url.rstrip("/"), headers, HTTPClient.METHOD_POST, JSON.stringify({"amountCents": amount_cents})) if error != OK: checkout_pending = false _close_browser_window() donation_failed.emit("The secure checkout request could not be started.") func stop_polling() -> void: poll_timer.stop() poll_count = 0 if status_pending and status_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: status_request.cancel_request() status_pending = false func resume_polling() -> void: if active_donation_id.is_empty() or poll_timer == null: return poll_timer.start() _poll_status() func _on_checkout_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: checkout_pending = false if response_code == 401: AuthState.handle_unauthorized(request_token) if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: _close_browser_window() donation_failed.emit(_failure_message("Checkout could not be started.", result, response_code, body)) return var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if typeof(parsed) != TYPE_DICTIONARY: _close_browser_window() donation_failed.emit("The donation service returned an invalid response.") return var donation_id := str(parsed.get("donationId", "")) var checkout_url := str(parsed.get("checkoutUrl", "")) if donation_id.is_empty() or not checkout_url.begins_with("https://"): _close_browser_window() donation_failed.emit("The donation service returned an invalid checkout link.") return active_donation_id = donation_id _open_checkout_url(checkout_url) checkout_started.emit(donation_id, checkout_url) poll_count = 0 poll_timer.start() _poll_status() func _poll_status() -> void: if status_pending or active_donation_id.is_empty() or not AuthState.can_send_authenticated_request(): return poll_count += 1 if poll_count > POLL_LIMIT: stop_polling() return status_pending = true request_token = AuthState.access_token var headers := PackedStringArray(["Authorization: Bearer %s" % request_token]) var error := status_request.request("%s/%s" % [api_url.rstrip("/"), active_donation_id], headers) if error != OK: status_pending = false func _on_status_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: status_pending = false if response_code == 401: AuthState.handle_unauthorized(request_token) stop_polling() return if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: return var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if typeof(parsed) != TYPE_DICTIONARY: return var status := str(parsed.get("status", "")) donation_status_changed.emit(status, parsed) if status in ["paid", "failed", "expired"]: stop_polling() func _prepare_browser_window() -> void: if OS.has_feature("web"): JavaScriptBridge.eval( "window.localStorage.removeItem('%s'); window.medmindDonationWindow = window.open('about:blank', 'medmind_donation_checkout'); if (window.medmindDonationWindow) { var medmindWaitingWindow = window.medmindDonationWindow; medmindWaitingWindow.document.title = 'MedMind secure checkout'; medmindWaitingWindow.document.body.innerHTML = '
Opening secure checkout...

This window will continue to Stripe.

'; medmindWaitingWindow.setInterval(function () { var checkoutUrl = medmindWaitingWindow.localStorage.getItem('%s'); if (checkoutUrl) { var checkout = medmindWaitingWindow.document.getElementById('medmind-checkout'); checkout.innerHTML = 'Secure checkout is ready.

Continue to Stripe

'; medmindWaitingWindow.document.getElementById('medmind-checkout-link').href = checkoutUrl; medmindWaitingWindow.localStorage.removeItem('%s'); medmindWaitingWindow.location.replace(checkoutUrl); } }, 150); }" % [CHECKOUT_STORAGE_KEY, CHECKOUT_STORAGE_KEY, CHECKOUT_STORAGE_KEY], true) func _open_checkout_url(url: String) -> void: if OS.has_feature("web"): JavaScriptBridge.eval( "var medmindCheckoutUrl = %s; window.localStorage.setItem('%s', medmindCheckoutUrl); if (window.medmindDonationWindow && !window.medmindDonationWindow.closed) { window.medmindDonationWindow.focus(); } else { var medmindCheckoutWindow = window.open(medmindCheckoutUrl, 'medmind_donation_checkout'); if (medmindCheckoutWindow) { window.medmindDonationWindow = medmindCheckoutWindow; medmindCheckoutWindow.focus(); } else { window.location.assign(medmindCheckoutUrl); } }" % [JSON.stringify(url), CHECKOUT_STORAGE_KEY], true) else: OS.shell_open(url) func _close_browser_window() -> void: if OS.has_feature("web"): JavaScriptBridge.eval( "window.localStorage.removeItem('%s'); if (window.medmindDonationWindow && !window.medmindDonationWindow.closed) { window.medmindDonationWindow.close(); } window.medmindDonationWindow = null;" % CHECKOUT_STORAGE_KEY, true) func _failure_message(prefix: String, result: int, response_code: int, body: PackedByteArray) -> String: if result != HTTPRequest.RESULT_SUCCESS and response_code == 0: return "%s Check your connection and try again." % prefix var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if typeof(parsed) == TYPE_DICTIONARY: var detail := str(parsed.get("detail", "")).strip_edges() if not detail.is_empty(): return detail return "%s Status %d." % [prefix, response_code] func _on_auth_changed() -> void: if not AuthState.is_logged_in: stop_polling() active_donation_id = ""