Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30ff63f3fd | |||
| 5c4a45f3c9 | |||
| 15ac91c42f | |||
| 70564ff799 |
@@ -0,0 +1,136 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/wallet_ui.gd" id="1_wui"]
|
||||||
|
|
||||||
|
[node name="WalletUI" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_wui")
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 32.0
|
||||||
|
offset_top = 32.0
|
||||||
|
offset_right = -32.0
|
||||||
|
offset_bottom = -32.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
|
||||||
|
[node name="HeaderHBox" type="HBoxContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
|
||||||
|
[node name="BackButton" type="Button" parent="VBox/HeaderHBox"]
|
||||||
|
custom_minimum_size = Vector2(100, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "< Back"
|
||||||
|
|
||||||
|
[node name="TitleLabel" type="Label" parent="VBox/HeaderHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
|
text = "Wallet & Credits"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="RefreshButton" type="Button" parent="VBox/HeaderHBox"]
|
||||||
|
custom_minimum_size = Vector2(100, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Refresh"
|
||||||
|
|
||||||
|
[node name="StatusLabel" type="Label" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="BalanceCard" type="PanelContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Margin" type="MarginContainer" parent="VBox/BalanceCard"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 16
|
||||||
|
theme_override_constants/margin_top = 16
|
||||||
|
theme_override_constants/margin_right = 16
|
||||||
|
theme_override_constants/margin_bottom = 16
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="VBox/BalanceCard/Margin"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 20
|
||||||
|
|
||||||
|
[node name="BalTitle" type="Label" parent="VBox/BalanceCard/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
text = "Available Balance:"
|
||||||
|
|
||||||
|
[node name="BalanceLabel" type="Label" parent="VBox/BalanceCard/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 22
|
||||||
|
text = "0 CR"
|
||||||
|
|
||||||
|
[node name="Spacer" type="Control" parent="VBox/BalanceCard/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="PacksHBox" type="HBoxContainer" parent="VBox/BalanceCard/Margin/HBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="Buy100Btn" type="Button" parent="VBox/BalanceCard/Margin/HBox/PacksHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "+100 CR ($0.99)"
|
||||||
|
|
||||||
|
[node name="Buy500Btn" type="Button" parent="VBox/BalanceCard/Margin/HBox/PacksHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "+500 CR ($4.99)"
|
||||||
|
|
||||||
|
[node name="Buy1000Btn" type="Button" parent="VBox/BalanceCard/Margin/HBox/PacksHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "+1,000 CR ($9.99)"
|
||||||
|
|
||||||
|
[node name="Buy5000Btn" type="Button" parent="VBox/BalanceCard/Margin/HBox/PacksHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "+5,000 CR ($44.99)"
|
||||||
|
|
||||||
|
[node name="LedgerSection" type="VBoxContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="LedgerTitle" type="Label" parent="VBox/LedgerSection"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
text = "Ledger History"
|
||||||
|
|
||||||
|
[node name="Scroll" type="ScrollContainer" parent="VBox/LedgerSection"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="TxList" type="VBoxContainer" parent="VBox/LedgerSection/Scroll"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
|
[node name="PagingHBox" type="HBoxContainer" parent="VBox/LedgerSection"]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
|
||||||
|
[node name="PrevPageBtn" type="Button" parent="VBox/LedgerSection/PagingHBox"]
|
||||||
|
custom_minimum_size = Vector2(80, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Previous"
|
||||||
|
|
||||||
|
[node name="PageLabel" type="Label" parent="VBox/LedgerSection/PagingHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Page 1 of 1"
|
||||||
|
|
||||||
|
[node name="NextPageBtn" type="Button" parent="VBox/LedgerSection/PagingHBox"]
|
||||||
|
custom_minimum_size = Vector2(80, 32)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Next"
|
||||||
@@ -138,7 +138,8 @@ func request(
|
|||||||
path: String,
|
path: String,
|
||||||
body: String = "",
|
body: String = "",
|
||||||
content_type: String = "application/json",
|
content_type: String = "application/json",
|
||||||
require_auth: bool = false
|
require_auth: bool = false,
|
||||||
|
custom_headers: PackedStringArray = PackedStringArray()
|
||||||
) -> Dictionary:
|
) -> Dictionary:
|
||||||
var http := HTTPRequest.new()
|
var http := HTTPRequest.new()
|
||||||
http.timeout = timeout_seconds
|
http.timeout = timeout_seconds
|
||||||
@@ -152,6 +153,8 @@ func request(
|
|||||||
headers.append("Content-Type: " + content_type)
|
headers.append("Content-Type: " + content_type)
|
||||||
if require_auth and has_token():
|
if require_auth and has_token():
|
||||||
headers.append("Authorization: " + bearer_token)
|
headers.append("Authorization: " + bearer_token)
|
||||||
|
for ch in custom_headers:
|
||||||
|
headers.append(ch)
|
||||||
|
|
||||||
var url := _base_url + path
|
var url := _base_url + path
|
||||||
var err := http.request(url, headers, method, body)
|
var err := http.request(url, headers, method, body)
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name EconomyApiClient
|
||||||
|
## Helper class for AGP Economy REST endpoints (Wallet, Payments, Marketplace, Payouts).
|
||||||
|
##
|
||||||
|
## docs/planning/economy.md US-6 (task cc09c1428892).
|
||||||
|
|
||||||
|
static func get_api_client(node: Node) -> Node:
|
||||||
|
if node.has_node("/root/ApiClient"):
|
||||||
|
return node.get_node("/root/ApiClient")
|
||||||
|
return null
|
||||||
|
|
||||||
|
static func generate_idempotency_key() -> String:
|
||||||
|
return "%d-%d" % [Time.get_unix_time_from_system(), randi() % 1000000]
|
||||||
|
|
||||||
|
# ── Wallet & Currency ──
|
||||||
|
|
||||||
|
static func get_wallet(api: Node) -> Dictionary:
|
||||||
|
return await api.request(HTTPClient.METHOD_GET, "/api/wallet", "", "application/json", true)
|
||||||
|
|
||||||
|
static func get_wallet_transactions(api: Node, limit: int = 50, offset: int = 0) -> Dictionary:
|
||||||
|
return await api.request(HTTPClient.METHOD_GET, "/api/wallet/transactions?limit=%d&offset=%d" % [limit, offset], "", "application/json", true)
|
||||||
|
|
||||||
|
# ── Premium Purchases ──
|
||||||
|
|
||||||
|
static func purchase_credits(api: Node, credits: int, idempotency_key: String = "") -> Dictionary:
|
||||||
|
var custom_headers := PackedStringArray()
|
||||||
|
var key := idempotency_key if idempotency_key != "" else generate_idempotency_key()
|
||||||
|
custom_headers.append("Idempotency-Key: " + key)
|
||||||
|
|
||||||
|
var sess_res: Dictionary = await api.request(
|
||||||
|
HTTPClient.METHOD_POST,
|
||||||
|
"/api/payments/purchase",
|
||||||
|
JSON.stringify({"credits": credits}),
|
||||||
|
"application/json",
|
||||||
|
true,
|
||||||
|
custom_headers
|
||||||
|
)
|
||||||
|
if not sess_res.ok:
|
||||||
|
return sess_res
|
||||||
|
|
||||||
|
var sess_id: String = str(sess_res.data.get("session_id", ""))
|
||||||
|
if sess_id == "":
|
||||||
|
return {"ok": false, "status": 0, "data": null, "detail": "Invalid session returned"}
|
||||||
|
|
||||||
|
return await api.request(
|
||||||
|
HTTPClient.METHOD_POST,
|
||||||
|
"/api/payments/confirm",
|
||||||
|
JSON.stringify({"session_id": sess_id}),
|
||||||
|
"application/json",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Catalog Items & Listings ──
|
||||||
|
|
||||||
|
static func get_catalog_items(api: Node, mine: bool = false, item_type: String = "", limit: int = 50, offset: int = 0) -> Dictionary:
|
||||||
|
var q := "/api/marketplace/items?limit=%d&offset=%d" % [limit, offset]
|
||||||
|
if mine:
|
||||||
|
q += "&mine=true"
|
||||||
|
if item_type != "":
|
||||||
|
q += "&item_type=" + item_type.uri_encode()
|
||||||
|
return await api.request(HTTPClient.METHOD_GET, q, "", "application/json", true)
|
||||||
|
|
||||||
|
static func create_catalog_item(api: Node, name: String, item_type: String, description: String = "", metadata_json: Dictionary = {}) -> Dictionary:
|
||||||
|
var payload := {
|
||||||
|
"name": name,
|
||||||
|
"item_type": item_type,
|
||||||
|
"description": description,
|
||||||
|
"metadata_json": metadata_json
|
||||||
|
}
|
||||||
|
return await api.request(HTTPClient.METHOD_POST, "/api/marketplace/items", JSON.stringify(payload), "application/json", true)
|
||||||
|
|
||||||
|
static func create_listing(api: Node, catalog_item_id: int, price: int) -> Dictionary:
|
||||||
|
var payload := {
|
||||||
|
"catalog_item_id": catalog_item_id,
|
||||||
|
"price": price
|
||||||
|
}
|
||||||
|
return await api.request(HTTPClient.METHOD_POST, "/api/marketplace/listings", JSON.stringify(payload), "application/json", true)
|
||||||
|
|
||||||
|
static func search_listings(api: Node, query: String = "", item_type: String = "", min_price: int = -1, max_price: int = -1, limit: int = 50, offset: int = 0) -> Dictionary:
|
||||||
|
var q := "/api/marketplace/listings?limit=%d&offset=%d" % [limit, offset]
|
||||||
|
if query != "":
|
||||||
|
q += "&q=" + query.uri_encode()
|
||||||
|
if item_type != "":
|
||||||
|
q += "&item_type=" + item_type.uri_encode()
|
||||||
|
if min_price >= 0:
|
||||||
|
q += "&min_price=%d" % min_price
|
||||||
|
if max_price >= 0:
|
||||||
|
q += "&max_price=%d" % max_price
|
||||||
|
return await api.request(HTTPClient.METHOD_GET, q, "", "application/json", false)
|
||||||
|
|
||||||
|
static func get_listing(api: Node, listing_id: int) -> Dictionary:
|
||||||
|
return await api.request(HTTPClient.METHOD_GET, "/api/marketplace/listings/%d" % listing_id, "", "application/json", false)
|
||||||
|
|
||||||
|
static func update_listing(api: Node, listing_id: int, price: int = -1, status: String = "") -> Dictionary:
|
||||||
|
var payload := {}
|
||||||
|
if price > 0:
|
||||||
|
payload["price"] = price
|
||||||
|
if status != "":
|
||||||
|
payload["status"] = status
|
||||||
|
return await api.request(HTTPClient.METHOD_PATCH, "/api/marketplace/listings/%d" % listing_id, JSON.stringify(payload), "application/json", true)
|
||||||
|
|
||||||
|
static func delist_listing(api: Node, listing_id: int) -> Dictionary:
|
||||||
|
return await api.request(HTTPClient.METHOD_DELETE, "/api/marketplace/listings/%d" % listing_id, "", "application/json", true)
|
||||||
|
|
||||||
|
# ── Purchases & Escrow ──
|
||||||
|
|
||||||
|
static func purchase_listing(api: Node, listing_id: int, idempotency_key: String = "") -> Dictionary:
|
||||||
|
var custom_headers := PackedStringArray()
|
||||||
|
var key := idempotency_key if idempotency_key != "" else generate_idempotency_key()
|
||||||
|
custom_headers.append("Idempotency-Key: " + key)
|
||||||
|
|
||||||
|
return await api.request(
|
||||||
|
HTTPClient.METHOD_POST,
|
||||||
|
"/api/marketplace/listings/%d/purchase" % listing_id,
|
||||||
|
"",
|
||||||
|
"application/json",
|
||||||
|
true,
|
||||||
|
custom_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
static func get_purchases(api: Node, role: String = "all", limit: int = 50, offset: int = 0) -> Dictionary:
|
||||||
|
return await api.request(
|
||||||
|
HTTPClient.METHOD_GET,
|
||||||
|
"/api/marketplace/purchases?role=%s&limit=%d&offset=%d" % [role.uri_encode(), limit, offset],
|
||||||
|
"",
|
||||||
|
"application/json",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Creator Balances & Payouts ──
|
||||||
|
|
||||||
|
static func get_creator_balance(api: Node) -> Dictionary:
|
||||||
|
return await api.request(HTTPClient.METHOD_GET, "/api/marketplace/creator/balance", "", "application/json", true)
|
||||||
|
|
||||||
|
static func request_payout(api: Node, amount: int, external_ref: String = "") -> Dictionary:
|
||||||
|
var payload := {"amount": amount}
|
||||||
|
if external_ref != "":
|
||||||
|
payload["external_ref"] = external_ref
|
||||||
|
return await api.request(HTTPClient.METHOD_POST, "/api/payouts", JSON.stringify(payload), "application/json", true)
|
||||||
|
|
||||||
|
static func get_payouts(api: Node, limit: int = 50, offset: int = 0) -> Dictionary:
|
||||||
|
return await api.request(HTTPClient.METHOD_GET, "/api/payouts?limit=%d&offset=%d" % [limit, offset], "", "application/json", true)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Headless smoke test for EconomyApiClient.
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
print("[SMOKE] Starting EconomyApiClient smoke test...")
|
||||||
|
|
||||||
|
var api_script = load("res://scripts/api_client.gd")
|
||||||
|
if not api_script:
|
||||||
|
printerr("[FAIL] Could not load api_client.gd")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var api: Node = api_script.new()
|
||||||
|
root.add_child(api)
|
||||||
|
|
||||||
|
# Verify idempotency key generation
|
||||||
|
var key1: String = EconomyApiClient.generate_idempotency_key()
|
||||||
|
var key2: String = EconomyApiClient.generate_idempotency_key()
|
||||||
|
assert(key1 != "", "Key should not be empty")
|
||||||
|
assert(key1 != key2, "Keys should be unique")
|
||||||
|
print("[PASS] Idempotency key generation: ", key1)
|
||||||
|
|
||||||
|
# Verify method existence on EconomyApiClient
|
||||||
|
var client = EconomyApiClient.new()
|
||||||
|
assert(client.has_method("get_wallet"), "Missing get_wallet")
|
||||||
|
assert(client.has_method("get_wallet_transactions"), "Missing get_wallet_transactions")
|
||||||
|
assert(client.has_method("purchase_credits"), "Missing purchase_credits")
|
||||||
|
assert(client.has_method("get_catalog_items"), "Missing get_catalog_items")
|
||||||
|
assert(client.has_method("create_catalog_item"), "Missing create_catalog_item")
|
||||||
|
assert(client.has_method("create_listing"), "Missing create_listing")
|
||||||
|
assert(client.has_method("search_listings"), "Missing search_listings")
|
||||||
|
assert(client.has_method("get_listing"), "Missing get_listing")
|
||||||
|
assert(client.has_method("update_listing"), "Missing update_listing")
|
||||||
|
assert(client.has_method("delist_listing"), "Missing delist_listing")
|
||||||
|
assert(client.has_method("purchase_listing"), "Missing purchase_listing")
|
||||||
|
assert(client.has_method("get_purchases"), "Missing get_purchases")
|
||||||
|
assert(client.has_method("get_creator_balance"), "Missing get_creator_balance")
|
||||||
|
assert(client.has_method("request_payout"), "Missing request_payout")
|
||||||
|
assert(client.has_method("get_payouts"), "Missing get_payouts")
|
||||||
|
print("[PASS] All EconomyApiClient methods verified")
|
||||||
|
|
||||||
|
api.queue_free()
|
||||||
|
print("[SMOKE] EconomyApiClient smoke test PASSED")
|
||||||
|
quit(0)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Headless smoke test for WalletUI scene and script.
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run()
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
await process_frame
|
||||||
|
print("[SMOKE] Starting WalletUI smoke test...")
|
||||||
|
|
||||||
|
var scene = load("res://scenes/wallet_ui.tscn")
|
||||||
|
if not scene:
|
||||||
|
printerr("[FAIL] Could not load scenes/wallet_ui.tscn")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
var ui: Control = scene.instantiate()
|
||||||
|
root.add_child(ui)
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
# Verify onready nodes
|
||||||
|
assert(ui.back_button != null, "Missing back_button")
|
||||||
|
assert(ui.refresh_button != null, "Missing refresh_button")
|
||||||
|
assert(ui.balance_label != null, "Missing balance_label")
|
||||||
|
assert(ui.buy_100_btn != null, "Missing buy_100_btn")
|
||||||
|
assert(ui.buy_500_btn != null, "Missing buy_500_btn")
|
||||||
|
assert(ui.buy_1000_btn != null, "Missing buy_1000_btn")
|
||||||
|
assert(ui.buy_5000_btn != null, "Missing buy_5000_btn")
|
||||||
|
assert(ui.tx_container != null, "Missing tx_container")
|
||||||
|
assert(ui.prev_page_btn != null, "Missing prev_page_btn")
|
||||||
|
assert(ui.next_page_btn != null, "Missing next_page_btn")
|
||||||
|
assert(ui.page_label != null, "Missing page_label")
|
||||||
|
|
||||||
|
# Test rendering empty transactions
|
||||||
|
ui.render_transactions([])
|
||||||
|
assert(ui.tx_container.get_child_count() == 1, "Should have 1 empty notice child")
|
||||||
|
|
||||||
|
# Test rendering sample transactions
|
||||||
|
var sample_txs = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"created_at": "2026-09-15T12:00:00Z",
|
||||||
|
"type": "credit_purchase",
|
||||||
|
"amount": 500,
|
||||||
|
"balance_after": 500,
|
||||||
|
"reference_type": "payment",
|
||||||
|
"reference_id": "sess_123"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"created_at": "2026-09-15T12:15:00Z",
|
||||||
|
"type": "marketplace_purchase",
|
||||||
|
"amount": -200,
|
||||||
|
"balance_after": 300,
|
||||||
|
"reference_type": "marketplace_listing",
|
||||||
|
"reference_id": "45"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
ui.render_transactions(sample_txs)
|
||||||
|
assert(ui.tx_container.get_child_count() == 2, "Should have 2 transaction rows")
|
||||||
|
|
||||||
|
# Test paging logic
|
||||||
|
ui.total_tx_count = 45
|
||||||
|
ui.current_page = 0
|
||||||
|
ui._update_paging_controls()
|
||||||
|
assert(ui.prev_page_btn.disabled == true, "Prev should be disabled on page 0")
|
||||||
|
assert(ui.next_page_btn.disabled == false, "Next should be enabled on page 0")
|
||||||
|
assert("Page 1 of 3" in ui.page_label.text, "Page label text mismatch")
|
||||||
|
|
||||||
|
ui.queue_free()
|
||||||
|
print("[SMOKE] WalletUI smoke test PASSED")
|
||||||
|
quit(0)
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
extends Control
|
||||||
|
class_name WalletUI
|
||||||
|
## Wallet & Transaction History UI panel.
|
||||||
|
##
|
||||||
|
## Displays:
|
||||||
|
## - Current Credit balance
|
||||||
|
## - Credit pack quick purchases (100, 500, 1000, 5000 CR)
|
||||||
|
## - Paged ledger transaction history with status/amounts/references
|
||||||
|
##
|
||||||
|
## docs/planning/economy.md US-6 (task 34c725431645).
|
||||||
|
|
||||||
|
signal back_requested
|
||||||
|
|
||||||
|
@onready var back_button: Button = $VBox/HeaderHBox/BackButton
|
||||||
|
@onready var refresh_button: Button = $VBox/HeaderHBox/RefreshButton
|
||||||
|
@onready var status_label: Label = $VBox/StatusLabel
|
||||||
|
|
||||||
|
@onready var balance_label: Label = $VBox/BalanceCard/Margin/HBox/BalanceLabel
|
||||||
|
@onready var buy_100_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy100Btn
|
||||||
|
@onready var buy_500_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy500Btn
|
||||||
|
@onready var buy_1000_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy1000Btn
|
||||||
|
@onready var buy_5000_btn: Button = $VBox/BalanceCard/Margin/HBox/PacksHBox/Buy5000Btn
|
||||||
|
|
||||||
|
@onready var tx_container: VBoxContainer = $VBox/LedgerSection/Scroll/TxList
|
||||||
|
@onready var prev_page_btn: Button = $VBox/LedgerSection/PagingHBox/PrevPageBtn
|
||||||
|
@onready var next_page_btn: Button = $VBox/LedgerSection/PagingHBox/NextPageBtn
|
||||||
|
@onready var page_label: Label = $VBox/LedgerSection/PagingHBox/PageLabel
|
||||||
|
|
||||||
|
var _api_client: Node = null
|
||||||
|
var current_page: int = 0
|
||||||
|
const PAGE_SIZE: int = 20
|
||||||
|
var total_tx_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_api_client = _get_api()
|
||||||
|
_wire_signals()
|
||||||
|
if _api_client != null and _api_client.has_token():
|
||||||
|
refresh_all()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_api() -> Node:
|
||||||
|
if has_node("/root/ApiClient"):
|
||||||
|
return get_node("/root/ApiClient")
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _wire_signals() -> void:
|
||||||
|
if back_button and not back_button.pressed.is_connected(_on_back_pressed):
|
||||||
|
back_button.pressed.connect(_on_back_pressed)
|
||||||
|
if refresh_button and not refresh_button.pressed.is_connected(refresh_all):
|
||||||
|
refresh_button.pressed.connect(refresh_all)
|
||||||
|
|
||||||
|
if buy_100_btn and not buy_100_btn.pressed.is_connected(_buy_pack.bind(100)):
|
||||||
|
buy_100_btn.pressed.connect(_buy_pack.bind(100))
|
||||||
|
if buy_500_btn and not buy_500_btn.pressed.is_connected(_buy_pack.bind(500)):
|
||||||
|
buy_500_btn.pressed.connect(_buy_pack.bind(500))
|
||||||
|
if buy_1000_btn and not buy_1000_btn.pressed.is_connected(_buy_pack.bind(1000)):
|
||||||
|
buy_1000_btn.pressed.connect(_buy_pack.bind(1000))
|
||||||
|
if buy_5000_btn and not buy_5000_btn.pressed.is_connected(_buy_pack.bind(5000)):
|
||||||
|
buy_5000_btn.pressed.connect(_buy_pack.bind(5000))
|
||||||
|
|
||||||
|
if prev_page_btn and not prev_page_btn.pressed.is_connected(_on_prev_page):
|
||||||
|
prev_page_btn.pressed.connect(_on_prev_page)
|
||||||
|
if next_page_btn and not next_page_btn.pressed.is_connected(_on_next_page):
|
||||||
|
next_page_btn.pressed.connect(_on_next_page)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_back_pressed() -> void:
|
||||||
|
back_requested.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func show_status(text: String, is_error: bool = false) -> void:
|
||||||
|
if not status_label:
|
||||||
|
return
|
||||||
|
status_label.text = text
|
||||||
|
if is_error:
|
||||||
|
status_label.add_theme_color_override("font_color", Color(1.0, 0.3, 0.3))
|
||||||
|
else:
|
||||||
|
status_label.add_theme_color_override("font_color", Color(0.3, 0.9, 0.3))
|
||||||
|
|
||||||
|
|
||||||
|
func refresh_all() -> void:
|
||||||
|
refresh_wallet()
|
||||||
|
refresh_transactions()
|
||||||
|
|
||||||
|
|
||||||
|
func refresh_wallet() -> void:
|
||||||
|
var api = _get_api()
|
||||||
|
if not api or not api.has_token():
|
||||||
|
return
|
||||||
|
var res: Dictionary = await EconomyApiClient.get_wallet(api)
|
||||||
|
if res.ok and res.data is Dictionary:
|
||||||
|
var balance: int = int(res.data.get("balance", 0))
|
||||||
|
if balance_label:
|
||||||
|
balance_label.text = "%d CR" % balance
|
||||||
|
else:
|
||||||
|
show_status("Failed to fetch wallet: " + res.detail, true)
|
||||||
|
|
||||||
|
|
||||||
|
func refresh_transactions() -> void:
|
||||||
|
var api = _get_api()
|
||||||
|
if not api or not api.has_token():
|
||||||
|
return
|
||||||
|
var offset: int = current_page * PAGE_SIZE
|
||||||
|
var res: Dictionary = await EconomyApiClient.get_wallet_transactions(api, PAGE_SIZE, offset)
|
||||||
|
if res.ok and res.data is Dictionary:
|
||||||
|
var items: Array = res.data.get("items", [])
|
||||||
|
total_tx_count = int(res.data.get("total", 0))
|
||||||
|
render_transactions(items)
|
||||||
|
_update_paging_controls()
|
||||||
|
else:
|
||||||
|
show_status("Failed to fetch transactions: " + res.detail, true)
|
||||||
|
|
||||||
|
|
||||||
|
func _buy_pack(credits: int) -> void:
|
||||||
|
var api = _get_api()
|
||||||
|
if not api or not api.has_token():
|
||||||
|
show_status("Please login to purchase Credits", true)
|
||||||
|
return
|
||||||
|
show_status("Processing purchase of %d Credits..." % credits)
|
||||||
|
var res: Dictionary = await EconomyApiClient.purchase_credits(api, credits)
|
||||||
|
if res.ok and res.data is Dictionary:
|
||||||
|
var new_bal: int = int(res.data.get("new_balance", 0))
|
||||||
|
if balance_label:
|
||||||
|
balance_label.text = "%d CR" % new_bal
|
||||||
|
show_status("Successfully purchased %d Credits! Balance: %d CR" % [credits, new_bal])
|
||||||
|
refresh_transactions()
|
||||||
|
else:
|
||||||
|
show_status("Purchase failed: " + res.detail, true)
|
||||||
|
|
||||||
|
|
||||||
|
func render_transactions(tx_list: Array) -> void:
|
||||||
|
if not tx_container:
|
||||||
|
return
|
||||||
|
for c in tx_container.get_children():
|
||||||
|
tx_container.remove_child(c)
|
||||||
|
c.queue_free()
|
||||||
|
|
||||||
|
if tx_list.is_empty():
|
||||||
|
var empty_lbl := Label.new()
|
||||||
|
empty_lbl.text = "No transactions found."
|
||||||
|
empty_lbl.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
|
||||||
|
tx_container.add_child(empty_lbl)
|
||||||
|
return
|
||||||
|
|
||||||
|
for tx in tx_list:
|
||||||
|
if not tx is Dictionary:
|
||||||
|
continue
|
||||||
|
var row := PanelContainer.new()
|
||||||
|
var hbox := HBoxContainer.new()
|
||||||
|
hbox.add_theme_constant_override("separation", 12)
|
||||||
|
row.add_child(hbox)
|
||||||
|
|
||||||
|
var date_str: String = str(tx.get("created_at", ""))
|
||||||
|
if date_str.length() > 19:
|
||||||
|
date_str = date_str.substr(0, 19).replace("T", " ")
|
||||||
|
var date_lbl := Label.new()
|
||||||
|
date_lbl.text = date_str
|
||||||
|
date_lbl.custom_minimum_size = Vector2(140, 0)
|
||||||
|
date_lbl.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
||||||
|
hbox.add_child(date_lbl)
|
||||||
|
|
||||||
|
var type_lbl := Label.new()
|
||||||
|
type_lbl.text = str(tx.get("type", "")).replace("_", " ").capitalize()
|
||||||
|
type_lbl.custom_minimum_size = Vector2(160, 0)
|
||||||
|
hbox.add_child(type_lbl)
|
||||||
|
|
||||||
|
var amount: int = int(tx.get("amount", 0))
|
||||||
|
var amt_lbl := Label.new()
|
||||||
|
if amount > 0:
|
||||||
|
amt_lbl.text = "+%d CR" % amount
|
||||||
|
amt_lbl.add_theme_color_override("font_color", Color(0.2, 0.9, 0.2))
|
||||||
|
else:
|
||||||
|
amt_lbl.text = "%d CR" % amount
|
||||||
|
amt_lbl.add_theme_color_override("font_color", Color(0.9, 0.3, 0.3))
|
||||||
|
amt_lbl.custom_minimum_size = Vector2(100, 0)
|
||||||
|
hbox.add_child(amt_lbl)
|
||||||
|
|
||||||
|
var bal_lbl := Label.new()
|
||||||
|
bal_lbl.text = "Bal: %s CR" % str(tx.get("balance_after", 0))
|
||||||
|
bal_lbl.custom_minimum_size = Vector2(100, 0)
|
||||||
|
bal_lbl.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
|
||||||
|
hbox.add_child(bal_lbl)
|
||||||
|
|
||||||
|
var ref_lbl := Label.new()
|
||||||
|
var ref_type: String = str(tx.get("reference_type", ""))
|
||||||
|
var ref_id: String = str(tx.get("reference_id", ""))
|
||||||
|
if ref_type != "":
|
||||||
|
ref_lbl.text = "%s #%s" % [ref_type, ref_id]
|
||||||
|
else:
|
||||||
|
ref_lbl.text = "-"
|
||||||
|
ref_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
ref_lbl.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5))
|
||||||
|
hbox.add_child(ref_lbl)
|
||||||
|
|
||||||
|
tx_container.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_paging_controls() -> void:
|
||||||
|
var total_pages: int = int(ceil(float(total_tx_count) / float(PAGE_SIZE)))
|
||||||
|
if total_pages < 1:
|
||||||
|
total_pages = 1
|
||||||
|
if page_label:
|
||||||
|
page_label.text = "Page %d of %d" % [current_page + 1, total_pages]
|
||||||
|
if prev_page_btn:
|
||||||
|
prev_page_btn.disabled = current_page <= 0
|
||||||
|
if next_page_btn:
|
||||||
|
next_page_btn.disabled = current_page >= total_pages - 1
|
||||||
|
|
||||||
|
|
||||||
|
func _on_prev_page() -> void:
|
||||||
|
if current_page > 0:
|
||||||
|
current_page -= 1
|
||||||
|
refresh_transactions()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_next_page() -> void:
|
||||||
|
var total_pages: int = int(ceil(float(total_tx_count) / float(PAGE_SIZE)))
|
||||||
|
if current_page < total_pages - 1:
|
||||||
|
current_page += 1
|
||||||
|
refresh_transactions()
|
||||||
Reference in New Issue
Block a user