156 lines
5.3 KiB
GDScript
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 = ""
|