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 signal award_received(amount: int, reason: String) signal ledger_changed 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 ledger_request: HTTPRequest var request_token := "" var api_url := DEFAULT_URL var pending_store_action := "" var ledger: Array = [] var ledger_next_before := "" var ledger_append := false 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) ledger_request = HTTPRequest.new() ledger_request.timeout = 20.0 ledger_request.request_completed.connect(_on_ledger_completed) add_child(ledger_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 apply_award_response(parsed: Dictionary) -> void: var status: Variant = parsed.get("credits", null) if typeof(status) == TYPE_DICTIONARY: apply_status(status) for award_variant in parsed.get("creditAwards", []): if typeof(award_variant) != TYPE_DICTIONARY: continue var award: Dictionary = award_variant var amount := int(award.get("amount", 0)) if amount > 0: award_received.emit(amount, str(award.get("description", "Learning reward"))) func load_ledger(before: String = "") -> void: if not AuthState.can_send_authenticated_request() or ledger_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: return ledger_append = not before.is_empty() var suffix := "/ledger?limit=20" if not before.is_empty(): suffix += "&before=%s" % before.uri_encode() ledger_request.request(api_url + suffix, PackedStringArray(["Authorization: Bearer %s" % AuthState.access_token]), HTTPClient.METHOD_GET) func _on_ledger_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: credits_failed.emit("Coin history could not be loaded.") return var parsed: Variant = JSON.parse_string(body.get_string_from_utf8()) if typeof(parsed) != TYPE_DICTIONARY: return var items: Array = parsed.get("items", []) ledger = ledger + items if ledger_append else items var cursor: Variant = parsed.get("nextBefore", null) ledger_next_before = "" if cursor == null else str(cursor) ledger_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 = [] ledger = [] ledger_next_before = "" loaded = false loading = false if request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: request.cancel_request()