Files
step3/scripts/autoload/donation_service.gd
T

156 lines
5.3 KiB
GDScript

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
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
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
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:
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:
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://"):
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 _open_checkout_url(url: String) -> void:
if OS.has_feature("web"):
JavaScriptBridge.eval("window.location.assign(%s);" % JSON.stringify(url), true)
else:
OS.shell_open(url)
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 = ""