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 := "ranaze_stripe_checkout_url" 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 var checkout_popup_opened := 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 if OS.has_feature("web") and not _prepare_browser_window(): donation_failed.emit("Stripe checkout needs a popup. Allow popups for this site, then try again.") return 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() _close_browser_window() func _open_checkout_url(url: String) -> void: if OS.has_feature("web"): JavaScriptBridge.eval( "var ranazeStripeUrl = %s; window.localStorage.setItem('%s', ranazeStripeUrl); if (window.ranazeStripeWindow && !window.ranazeStripeWindow.closed) { try { window.ranazeStripeWindow.location.replace(ranazeStripeUrl); } catch (_) {} window.ranazeStripeWindow.focus(); } else { var ranazeStripePopup = window.open(ranazeStripeUrl, 'ranaze_stripe'); if (ranazeStripePopup) { window.ranazeStripeWindow = ranazeStripePopup; ranazeStripePopup.focus(); } else { window.location.assign(ranazeStripeUrl); } }" % [JSON.stringify(url), CHECKOUT_STORAGE_KEY], true ) else: OS.shell_open(url) func _prepare_browser_window() -> bool: var popup_result: Variant = JavaScriptBridge.eval( "window.localStorage.removeItem('%s'); var ranazeStripeBridge = new URL('/external-redirect.html?key=%s&provider=Stripe', window.location.origin).href; window.ranazeStripeWindow = window.open(ranazeStripeBridge, 'ranaze_stripe', 'popup=yes,width=620,height=760,resizable=yes,scrollbars=yes'); if (window.ranazeStripeWindow) { window.ranazeStripeWindow.blur(); window.focus(); } window.ranazeStripeWindow !== null;" % [CHECKOUT_STORAGE_KEY, CHECKOUT_STORAGE_KEY], true ) checkout_popup_opened = bool(popup_result) return checkout_popup_opened func _close_browser_window() -> void: if not OS.has_feature("web") or not checkout_popup_opened: return JavaScriptBridge.eval( "window.localStorage.removeItem('%s'); if (window.ranazeStripeWindow && !window.ranazeStripeWindow.closed) { window.ranazeStripeWindow.close(); } window.ranazeStripeWindow = null; window.focus();" % CHECKOUT_STORAGE_KEY, true ) checkout_popup_opened = false 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() _close_browser_window() active_donation_id = ""