extends Node const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd") const DEFAULT_URL := "https://us3q.ranaze.com/api/access" const REQUEST_TIMEOUT_SECONDS := 30.0 const POLL_INTERVAL_SECONDS := 3.0 const POLL_LIMIT := 200 const CHECKOUT_STORAGE_KEY := "ranaze_access_checkout_url" signal status_changed(status: Dictionary) signal operation_succeeded(message: String) signal operation_failed(message: String) signal checkout_started(purchase_id: String, checkout_url: String) var api_url := DEFAULT_URL var status: Dictionary = {} var status_request: HTTPRequest var action_request: HTTPRequest var purchase_request: HTTPRequest var poll_timer: Timer var pending_action := "" var request_token := "" var active_purchase_id := "" var poll_count := 0 var checkout_popup_opened := false func _ready() -> void: api_url = EndpointConfig.resolve( str(ProjectSettings.get_setting("step3/access_api_url", DEFAULT_URL)), "/api/access") status_request = _request_node(_on_status_completed) action_request = _request_node(_on_action_completed) purchase_request = _request_node(_on_purchase_status_completed) poll_timer = Timer.new() poll_timer.wait_time = POLL_INTERVAL_SECONDS poll_timer.timeout.connect(_poll_purchase) add_child(poll_timer) AuthState.auth_changed.connect(_on_auth_changed) if AuthState.is_logged_in: refresh() func refresh() -> void: if not AuthState.can_send_authenticated_request() or status_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: return request_token = AuthState.access_token status_request.request(api_url, PackedStringArray(["Authorization: Bearer %s" % request_token])) func redeem_credits() -> void: _send_action("redeem", "/redeem-credits", {"operationId": _operation_id("credit-pass")}) func activate_token(token: String) -> void: _send_action("activate", "/activate-token", { "token": token, "operationId": _operation_id("activate-%s" % token) }) func reset_qbank() -> void: _send_action("reset", "/qbank-reset", { "confirmation": "RESET_QBANK", "operationId": _operation_id("qbank-reset") }) func begin_checkout(sku: String) -> void: if OS.has_feature("steam"): _send_action("steam_checkout", "/steam-checkouts", {"sku": sku}) return if OS.has_feature("web") and not _prepare_browser_window(): operation_failed.emit("Checkout needs a popup. Allow popups for this site, then try again.") return _send_action("checkout", "/web-checkouts", {"sku": sku}) func apply_embedded_status(value: Variant) -> void: if typeof(value) != TYPE_DICTIONARY: return status = value.duplicate(true) status_changed.emit(status) func has_unlimited_access() -> bool: return bool(status.get("unlimited", false)) func questions_remaining() -> int: return int(status.get("questionsRemaining", 20)) func _send_action(action: String, path: String, payload: Dictionary) -> void: if pending_action != "": operation_failed.emit("Another access request is still in progress.") return if not AuthState.can_send_authenticated_request(): operation_failed.emit("Log in before managing access.") return pending_action = action request_token = AuthState.access_token var error := action_request.request( "%s%s" % [api_url.rstrip("/"), path], PackedStringArray([ "Content-Type: application/json", "Authorization: Bearer %s" % request_token ]), HTTPClient.METHOD_POST, JSON.stringify(payload)) if error != OK: pending_action = "" operation_failed.emit("The access request could not be started.") func _on_status_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: if response_code == 401: AuthState.handle_unauthorized(request_token) 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()) apply_embedded_status(parsed) func _on_action_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: var action := pending_action pending_action = "" 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() operation_failed.emit(_failure_message("Access request failed.", response_code, body)) return var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if action == "checkout": if typeof(parsed) != TYPE_DICTIONARY: _close_browser_window() operation_failed.emit("Checkout returned an invalid response.") return active_purchase_id = str(parsed.get("purchaseId", "")) var checkout_url := str(parsed.get("checkoutUrl", "")) if active_purchase_id.is_empty() or not checkout_url.begins_with("https://"): _close_browser_window() operation_failed.emit("Checkout returned an invalid link.") return _open_checkout_url(checkout_url) checkout_started.emit(active_purchase_id, checkout_url) poll_count = 0 poll_timer.start() _poll_purchase() return if typeof(parsed) == TYPE_DICTIONARY: apply_embedded_status(parsed) operation_succeeded.emit({ "redeem": "Your 24-hour QBank pass is active.", "activate": "Your access token was activated.", "reset": "Your QBank has been reset for a new cycle." }.get(action, "Access updated.")) func _poll_purchase() -> void: if active_purchase_id.is_empty() or not AuthState.can_send_authenticated_request(): return if purchase_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: return poll_count += 1 if poll_count > POLL_LIMIT: poll_timer.stop() return request_token = AuthState.access_token purchase_request.request( "%s/purchases/%s" % [api_url.rstrip("/"), active_purchase_id.uri_encode()], PackedStringArray(["Authorization: Bearer %s" % request_token])) func _on_purchase_status_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: 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 match str(parsed.get("status", "")): "paid": poll_timer.stop() _close_browser_window() active_purchase_id = "" operation_succeeded.emit("Purchase confirmed. Your token is ready to activate.") refresh() "failed", "expired", "refunded": poll_timer.stop() _close_browser_window() active_purchase_id = "" operation_failed.emit("The purchase was not completed.") func _request_node(callback: Callable) -> HTTPRequest: var node := HTTPRequest.new() node.timeout = REQUEST_TIMEOUT_SECONDS node.request_completed.connect(callback) add_child(node) return node func _operation_id(prefix: String) -> String: return "%s-%d-%d" % [prefix, Time.get_ticks_usec(), randi()] func _open_checkout_url(url: String) -> void: if OS.has_feature("web"): JavaScriptBridge.eval( "var ranazeUrl=%s;window.localStorage.setItem('%s',ranazeUrl);if(window.ranazeAccessWindow&&!window.ranazeAccessWindow.closed){window.ranazeAccessWindow.location.replace(ranazeUrl);window.ranazeAccessWindow.focus();}else{window.location.assign(ranazeUrl);}" % [JSON.stringify(url), CHECKOUT_STORAGE_KEY], true) else: OS.shell_open(url) func _prepare_browser_window() -> bool: checkout_popup_opened = bool(JavaScriptBridge.eval( "window.ranazeAccessWindow=window.open('about:blank','ranaze_access','popup=yes,width=620,height=760,resizable=yes,scrollbars=yes');window.ranazeAccessWindow!==null;", true)) return checkout_popup_opened func _close_browser_window() -> void: if OS.has_feature("web") and checkout_popup_opened: JavaScriptBridge.eval( "window.localStorage.removeItem('%s');if(window.ranazeAccessWindow&&!window.ranazeAccessWindow.closed){window.ranazeAccessWindow.close();}window.ranazeAccessWindow=null;window.focus();" % CHECKOUT_STORAGE_KEY, true) checkout_popup_opened = false func _failure_message(prefix: String, response_code: int, body: PackedByteArray) -> String: var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if typeof(parsed) == TYPE_DICTIONARY: var message := str(parsed.get("message", parsed.get("detail", ""))).strip_edges() if not message.is_empty(): return message return "%s Status %d." % [prefix, response_code] func _on_auth_changed() -> void: if AuthState.is_logged_in: refresh() else: status = {} active_purchase_id = "" poll_timer.stop() _close_browser_window() status_changed.emit(status)