extends Node signal credits_changed signal credits_failed(message: String) signal store_operation_completed(message: String) signal spin_resolved(reward_key: String, reward_label: String, coins_won: int) signal spin_config_loaded const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd") const DEFAULT_URL := "https://us3q.ranaze.com/api/credits" var credits: int = 0 var exam_tokens: int = 0 var qbank_reset_tokens: int = 0 var bank_cycle: int = 0 var products: Array = [] var spin_cost := 50 var spin_rewards: Array = [] var loaded := false var loading := false var request: HTTPRequest var action_request: HTTPRequest var spin_config_request: HTTPRequest var request_token := "" var api_url := DEFAULT_URL var pending_store_action := "" func _ready() -> void: api_url = EndpointConfig.resolve( str(ProjectSettings.get_setting("step3/credits_api_url", DEFAULT_URL)), "/api/credits") request = HTTPRequest.new() request.timeout = 20.0 request.request_completed.connect(_on_completed) add_child(request) action_request = HTTPRequest.new() action_request.timeout = 20.0 action_request.request_completed.connect(_on_action_completed) add_child(action_request) spin_config_request = HTTPRequest.new() spin_config_request.timeout = 20.0 spin_config_request.request_completed.connect(_on_spin_config_completed) add_child(spin_config_request) AuthState.auth_changed.connect(_on_auth_changed) func refresh() -> void: if loading or not AuthState.can_send_authenticated_request(): return loading = true request_token = AuthState.access_token var headers := PackedStringArray(["Authorization: Bearer %s" % request_token]) var error := request.request(api_url, headers, HTTPClient.METHOD_GET) if error != OK: loading = false credits_failed.emit("Points balance could not be loaded. Check your connection and try again.") func apply_status(parsed: Dictionary) -> void: credits = int(parsed.get("credits", credits)) exam_tokens = int(parsed.get("examTokens", exam_tokens)) qbank_reset_tokens = int(parsed.get("qBankResetTokens", qbank_reset_tokens)) bank_cycle = int(parsed.get("bankCycle", bank_cycle)) products = parsed.get("products", products) loaded = true credits_changed.emit() func _on_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: loading = false if response_code == 401: AuthState.handle_unauthorized(request_token) return if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: credits_failed.emit("Points balance could not be loaded. Check your connection and try again.") return var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if typeof(parsed) != TYPE_DICTIONARY: credits_failed.emit("The credits service returned an invalid response.") return apply_status(parsed) func purchase(sku: String) -> void: pending_store_action = "purchase" _post("/purchases", {"sku": sku, "operationId": _operation_id("purchase")}) func reset_qbank() -> void: pending_store_action = "reset" _post("/qbank-reset", {"confirmation": "RESET_QBANK", "operationId": _operation_id("reset")}) func spin() -> void: pending_store_action = "spin" _post("/spin", {"operationId": _operation_id("spin")}) func fetch_spin_config() -> void: if not AuthState.can_send_authenticated_request(): return var headers := PackedStringArray(["Authorization: Bearer %s" % AuthState.access_token]) spin_config_request.request(api_url + "/spin-config", headers, HTTPClient.METHOD_GET) func _on_spin_config_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 spin_cost = int(parsed.get("spinCost", spin_cost)) spin_rewards = parsed.get("rewards", spin_rewards) spin_config_loaded.emit() func _post(path: String, payload: Dictionary) -> void: if not AuthState.can_send_authenticated_request(): pending_store_action = "" credits_failed.emit("You're signed out. Sign back in and try again.") return var headers := PackedStringArray([ "Authorization: Bearer %s" % AuthState.access_token, "Content-Type: application/json" ]) var error := action_request.request(api_url + path, headers, HTTPClient.METHOD_POST, JSON.stringify(payload)) if error != OK: credits_failed.emit("The Rz Coin Store could not complete that action.") func _on_action_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if result != HTTPRequest.RESULT_SUCCESS: credits_failed.emit("The Rz Coin Store could not complete that action.") return if response_code >= 200 and response_code < 300 and typeof(parsed) == TYPE_DICTIONARY: if pending_store_action == "spin": var status: Variant = parsed.get("status", {}) if typeof(status) == TYPE_DICTIONARY: apply_status(status) pending_store_action = "" spin_resolved.emit( str(parsed.get("rewardKey", "")), str(parsed.get("rewardLabel", "")), int(parsed.get("coinsWon", 0))) return apply_status(parsed) if pending_store_action == "reset": ProfileService.reset_qbank_eligibility() pending_store_action = "" store_operation_completed.emit("Your Rz Coin Store balance was updated.") return if typeof(parsed) == TYPE_DICTIONARY: var status: Variant = parsed.get("credits", {}) if typeof(status) == TYPE_DICTIONARY: apply_status(status) credits_failed.emit(str(parsed.get("message", "The store action could not be completed."))) else: credits_failed.emit("The store action could not be completed.") pending_store_action = "" func _operation_id(prefix: String) -> String: return "%s:%s:%d" % [prefix, str(AuthState.username), Time.get_ticks_usec()] func _on_auth_changed() -> void: if not AuthState.is_logged_in: credits = 0 exam_tokens = 0 qbank_reset_tokens = 0 bank_cycle = 0 products = [] spin_rewards = [] loaded = false loading = false if request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: request.cancel_request()