diff --git a/bun.lock b/bun.lock
index 8fef42a..4549c0f 100644
--- a/bun.lock
+++ b/bun.lock
@@ -12,9 +12,7 @@
"@angular/material": "^20.2.10",
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
- "@perawallet/connect": "^1.4.2",
"@tailwindcss/postcss": "^4.1.17",
- "algosdk": "^3.5.2",
"multiformats": "^13.4.1",
"nes.css": "^2.3.0",
"pixelarticons": "^1.8.1",
@@ -146,8 +144,6 @@
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, ""],
- "@evanhahn/lottie-web-light": ["@evanhahn/lottie-web-light@5.8.1", "", {}, "sha512-U0G1tt3/UEYnyCNNslWPi1dB7X1xQ9aoSip+B3GTKO/Bns8yz/p39vBkRSN9d25nkbHuCsbjky2coQftj5YVKw=="],
-
"@inquirer/ansi": ["@inquirer/ansi@1.0.0", "", {}, ""],
"@inquirer/checkbox": ["@inquirer/checkbox@4.2.4", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/core": "^10.2.2", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" } }, ""],
@@ -212,10 +208,6 @@
"@napi-rs/nice-darwin-arm64": ["@napi-rs/nice-darwin-arm64@1.1.1", "", { "os": "darwin", "cpu": "arm64" }, ""],
- "@noble/ciphers": ["@noble/ciphers@1.2.0", "", {}, "sha512-YGdEUzYEd+82jeaVbSKKVp1jFZb8LwaNMIIzHFkihGvYdd/KKAr7KaJHdEdSYGredE3ssSravXIa0Jxg28Sv5w=="],
-
- "@noble/hashes": ["@noble/hashes@1.7.0", "", {}, "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w=="],
-
"@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, ""],
"@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, ""],
@@ -240,8 +232,6 @@
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, ""],
- "@perawallet/connect": ["@perawallet/connect@1.4.2", "", { "dependencies": { "@evanhahn/lottie-web-light": "5.8.1", "@walletconnect/client": "^1.8.0", "@walletconnect/types": "^1.8.0", "bowser": "2.11.0", "buffer": "^6.0.3", "qr-code-styling": "1.6.0-rc.1" }, "peerDependencies": { "algosdk": "^3.0.0" } }, "sha512-LCdaWMm1PerIxnBWTkPVEfEbEJ+onfIfDK80+Nj//Rce//tLFzGU7kAxSU7Al70hrD1AvnYyXm2svFbsUzOa7w=="],
-
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, ""],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.38", "", { "os": "darwin", "cpu": "arm64" }, ""],
@@ -310,38 +300,6 @@
"@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@2.1.0", "", { "peerDependencies": { "vite": "^6.0.0 || ^7.0.0" } }, ""],
- "@walletconnect/browser-utils": ["@walletconnect/browser-utils@1.8.0", "", { "dependencies": { "@walletconnect/safe-json": "1.0.0", "@walletconnect/types": "^1.8.0", "@walletconnect/window-getters": "1.0.0", "@walletconnect/window-metadata": "1.0.0", "detect-browser": "5.2.0" } }, "sha512-Wcqqx+wjxIo9fv6eBUFHPsW1y/bGWWRboni5dfD8PtOmrihrEpOCmvRJe4rfl7xgJW8Ea9UqKEaq0bIRLHlK4A=="],
-
- "@walletconnect/client": ["@walletconnect/client@1.8.0", "", { "dependencies": { "@walletconnect/core": "^1.8.0", "@walletconnect/iso-crypto": "^1.8.0", "@walletconnect/types": "^1.8.0", "@walletconnect/utils": "^1.8.0" } }, "sha512-svyBQ14NHx6Cs2j4TpkQaBI/2AF4+LXz64FojTjMtV4VMMhl81jSO1vNeg+yYhQzvjcGH/GpSwixjyCW0xFBOQ=="],
-
- "@walletconnect/core": ["@walletconnect/core@1.8.0", "", { "dependencies": { "@walletconnect/socket-transport": "^1.8.0", "@walletconnect/types": "^1.8.0", "@walletconnect/utils": "^1.8.0" } }, "sha512-aFTHvEEbXcZ8XdWBw6rpQDte41Rxwnuk3SgTD8/iKGSRTni50gI9S3YEzMj05jozSiOBxQci4pJDMVhIUMtarw=="],
-
- "@walletconnect/crypto": ["@walletconnect/crypto@1.1.0", "", { "dependencies": { "@noble/ciphers": "1.2.0", "@noble/hashes": "1.7.0", "@walletconnect/encoding": "^1.0.2", "@walletconnect/environment": "^1.0.1", "@walletconnect/randombytes": "^1.0.3", "tslib": "1.14.1" } }, "sha512-yZO8BBTQt7BcaemjDgwN56OmSv0OO4QjIpvtfj5OxZfL6IQZQWHOhwC6pJg+BmZPbDlJlWFqFuCZRtiPwRmsoA=="],
-
- "@walletconnect/encoding": ["@walletconnect/encoding@1.0.2", "", { "dependencies": { "is-typedarray": "1.0.0", "tslib": "1.14.1", "typedarray-to-buffer": "3.1.5" } }, "sha512-CrwSBrjqJ7rpGQcTL3kU+Ief+Bcuu9PH6JLOb+wM6NITX1GTxR/MfNwnQfhLKK6xpRAyj2/nM04OOH6wS8Imag=="],
-
- "@walletconnect/environment": ["@walletconnect/environment@1.0.1", "", { "dependencies": { "tslib": "1.14.1" } }, "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg=="],
-
- "@walletconnect/iso-crypto": ["@walletconnect/iso-crypto@1.8.0", "", { "dependencies": { "@walletconnect/crypto": "^1.0.2", "@walletconnect/types": "^1.8.0", "@walletconnect/utils": "^1.8.0" } }, "sha512-pWy19KCyitpfXb70hA73r9FcvklS+FvO9QUIttp3c2mfW8frxgYeRXfxLRCIQTkaYueRKvdqPjbyhPLam508XQ=="],
-
- "@walletconnect/jsonrpc-types": ["@walletconnect/jsonrpc-types@1.0.4", "", { "dependencies": { "events": "^3.3.0", "keyvaluestorage-interface": "^1.0.0" } }, "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ=="],
-
- "@walletconnect/jsonrpc-utils": ["@walletconnect/jsonrpc-utils@1.0.8", "", { "dependencies": { "@walletconnect/environment": "^1.0.1", "@walletconnect/jsonrpc-types": "^1.0.3", "tslib": "1.14.1" } }, "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw=="],
-
- "@walletconnect/randombytes": ["@walletconnect/randombytes@1.1.0", "", { "dependencies": { "@noble/hashes": "1.7.0", "@walletconnect/encoding": "^1.0.2", "@walletconnect/environment": "^1.0.1", "tslib": "1.14.1" } }, "sha512-X+LO/9ClnXX2Q/1+u83qMnohVaxC4qsXByM/gMSwGMrUObxEiqEWS+b9Upg9oNl6mTr85dTCRF8W17KVcKKXQw=="],
-
- "@walletconnect/safe-json": ["@walletconnect/safe-json@1.0.0", "", {}, "sha512-QJzp/S/86sUAgWY6eh5MKYmSfZaRpIlmCJdi5uG4DJlKkZrHEF7ye7gA+VtbVzvTtpM/gRwO2plQuiooIeXjfg=="],
-
- "@walletconnect/socket-transport": ["@walletconnect/socket-transport@1.8.0", "", { "dependencies": { "@walletconnect/types": "^1.8.0", "@walletconnect/utils": "^1.8.0", "ws": "7.5.3" } }, "sha512-5DyIyWrzHXTcVp0Vd93zJ5XMW61iDM6bcWT4p8DTRfFsOtW46JquruMhxOLeCOieM4D73kcr3U7WtyR4JUsGuQ=="],
-
- "@walletconnect/types": ["@walletconnect/types@1.8.0", "", {}, "sha512-Cn+3I0V0vT9ghMuzh1KzZvCkiAxTq+1TR2eSqw5E5AVWfmCtECFkVZBP6uUJZ8YjwLqXheI+rnjqPy7sVM4Fyg=="],
-
- "@walletconnect/utils": ["@walletconnect/utils@1.8.0", "", { "dependencies": { "@walletconnect/browser-utils": "^1.8.0", "@walletconnect/encoding": "^1.0.1", "@walletconnect/jsonrpc-utils": "^1.0.3", "@walletconnect/types": "^1.8.0", "bn.js": "4.11.8", "js-sha3": "0.8.0", "query-string": "6.13.5" } }, "sha512-zExzp8Mj1YiAIBfKNm5u622oNw44WOESzo6hj+Q3apSMIb0Jph9X3GDIdbZmvVZsNPxWDL7uodKgZcCInZv2vA=="],
-
- "@walletconnect/window-getters": ["@walletconnect/window-getters@1.0.0", "", {}, "sha512-xB0SQsLaleIYIkSsl43vm8EwETpBzJ2gnzk7e0wMF3ktqiTGS6TFHxcprMl5R44KKh4tCcHCJwolMCaDSwtAaA=="],
-
- "@walletconnect/window-metadata": ["@walletconnect/window-metadata@1.0.0", "", { "dependencies": { "@walletconnect/window-getters": "^1.0.0" } }, "sha512-9eFvmJxIKCC3YWOL97SgRkKhlyGXkrHwamfechmqszbypFspaSk+t2jQXAEU7YClHF6Qjw5eYOmy1//zFi9/GA=="],
-
"@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, ""],
"abbrev": ["abbrev@3.0.1", "", {}, ""],
@@ -356,10 +314,6 @@
"algoliasearch": ["algoliasearch@5.35.0", "", { "dependencies": { "@algolia/abtesting": "1.1.0", "@algolia/client-abtesting": "5.35.0", "@algolia/client-analytics": "5.35.0", "@algolia/client-common": "5.35.0", "@algolia/client-insights": "5.35.0", "@algolia/client-personalization": "5.35.0", "@algolia/client-query-suggestions": "5.35.0", "@algolia/client-search": "5.35.0", "@algolia/ingestion": "1.35.0", "@algolia/monitoring": "1.35.0", "@algolia/recommend": "5.35.0", "@algolia/requester-browser-xhr": "5.35.0", "@algolia/requester-fetch": "5.35.0", "@algolia/requester-node-http": "5.35.0" } }, ""],
- "algorand-msgpack": ["algorand-msgpack@1.1.0", "", {}, "sha512-08k7pBQnkaUB5p+jL7f1TRaUIlTSDE0cesFu1mD7llLao+1cAhtvvZmGE3OnisTd0xOn118QMw74SRqddqaYvw=="],
-
- "algosdk": ["algosdk@3.5.2", "", { "dependencies": { "algorand-msgpack": "^1.1.0", "hi-base32": "^0.5.1", "js-sha256": "^0.9.0", "js-sha3": "^0.8.0", "js-sha512": "^0.8.0", "json-bigint": "^1.0.0", "tweetnacl": "^1.0.3", "vlq": "^2.0.4" } }, "sha512-frhGtZl1JvfrLRKmMvUm880wj4OiWsWo2FhbreNWh7pdFsKuWPj60fV682wt/CYefLI70iwHavPOwGBkTVt0VA=="],
-
"ansi-escapes": ["ansi-escapes@7.1.1", "", { "dependencies": { "environment": "^1.0.0" } }, ""],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, ""],
@@ -380,18 +334,12 @@
"beasties": ["beasties@0.3.5", "", { "dependencies": { "css-select": "^6.0.0", "css-what": "^7.0.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "htmlparser2": "^10.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.49", "postcss-media-query-parser": "^0.2.3" } }, ""],
- "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
-
"binary-extensions": ["binary-extensions@2.3.0", "", {}, ""],
- "bn.js": ["bn.js@4.11.8", "", {}, "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="],
-
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, ""],
"boolbase": ["boolbase@1.0.0", "", {}, ""],
- "bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="],
-
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, ""],
@@ -464,14 +412,10 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""],
- "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="],
-
"depd": ["depd@2.0.0", "", {}, ""],
"destroy": ["destroy@1.2.0", "", {}, ""],
- "detect-browser": ["detect-browser@5.2.0", "", {}, "sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA=="],
-
"detect-libc": ["detect-libc@1.0.3", "", { "bin": "bin/detect-libc.js" }, ""],
"di": ["di@0.0.1", "", {}, ""],
@@ -532,8 +476,6 @@
"eventemitter3": ["eventemitter3@5.0.1", "", {}, ""],
- "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
-
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, ""],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, ""],
@@ -606,8 +548,6 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""],
- "hi-base32": ["hi-base32@0.5.1", "", {}, "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA=="],
-
"hosted-git-info": ["hosted-git-info@9.0.0", "", { "dependencies": { "lru-cache": "^11.1.0" } }, ""],
"html-escaper": ["html-escaper@2.0.2", "", {}, ""],
@@ -662,8 +602,6 @@
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, ""],
- "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="],
-
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, ""],
"isbinaryfile": ["isbinaryfile@4.0.10", "", {}, ""],
@@ -686,18 +624,10 @@
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
- "js-sha256": ["js-sha256@0.9.0", "", {}, "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="],
-
- "js-sha3": ["js-sha3@0.8.0", "", {}, "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="],
-
- "js-sha512": ["js-sha512@0.8.0", "", {}, "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ=="],
-
"js-tokens": ["js-tokens@4.0.0", "", {}, ""],
"jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, ""],
- "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
-
"json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, ""],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, ""],
@@ -720,8 +650,6 @@
"karma-jasmine-html-reporter": ["karma-jasmine-html-reporter@2.1.0", "", { "peerDependencies": { "jasmine-core": "^4.0.0 || ^5.0.0", "karma": "^6.0.0", "karma-jasmine": "^5.0.0" } }, ""],
- "keyvaluestorage-interface": ["keyvaluestorage-interface@1.0.0", "", {}, "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g=="],
-
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
@@ -912,14 +840,8 @@
"qjobs": ["qjobs@1.2.0", "", {}, ""],
- "qr-code-styling": ["qr-code-styling@1.6.0-rc.1", "", { "dependencies": { "qrcode-generator": "^1.4.3" } }, "sha512-ModRIiW6oUnsP18QzrRYZSc/CFKFKIdj7pUs57AEVH20ajlglRpN3HukjHk0UbNMTlKGuaYl7Gt6/O5Gg2NU2Q=="],
-
- "qrcode-generator": ["qrcode-generator@1.5.2", "", {}, "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw=="],
-
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, ""],
- "query-string": ["query-string@6.13.5", "", { "dependencies": { "decode-uri-component": "^0.2.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-svk3xg9qHR39P3JlHuD7g3nRnyay5mHbrPctEBDUxUkHRifPHXJDhBUycdCC0NBjXoDf44Gb+IsOZL1Uwn8M/Q=="],
-
"range-parser": ["range-parser@1.2.1", "", {}, ""],
"raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, ""],
@@ -1012,8 +934,6 @@
"spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, ""],
- "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="],
-
"ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, ""],
"statuses": ["statuses@2.0.2", "", {}, ""],
@@ -1022,8 +942,6 @@
"streamroller": ["streamroller@3.1.5", "", { "dependencies": { "date-format": "^4.0.14", "debug": "^4.3.4", "fs-extra": "^8.1.0" } }, ""],
- "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="],
-
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, ""],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""],
@@ -1054,12 +972,8 @@
"tuf-js": ["tuf-js@3.1.0", "", { "dependencies": { "@tufjs/models": "3.0.1", "debug": "^4.4.1", "make-fetch-happen": "^14.0.3" } }, ""],
- "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="],
-
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, ""],
- "typedarray-to-buffer": ["typedarray-to-buffer@3.1.5", "", { "dependencies": { "is-typedarray": "^1.0.0" } }, "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q=="],
-
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""],
"ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": "script/cli.js" }, ""],
@@ -1088,8 +1002,6 @@
"vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "less", "lightningcss", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, ""],
- "vlq": ["vlq@2.0.4", "", {}, "sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA=="],
-
"void-elements": ["void-elements@2.0.1", "", {}, ""],
"watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, ""],
@@ -1180,18 +1092,6 @@
"@types/cors/@types/node": ["@types/node@24.6.1", "", { "dependencies": { "undici-types": "~7.13.0" } }, ""],
- "@walletconnect/crypto/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
-
- "@walletconnect/encoding/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
-
- "@walletconnect/environment/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
-
- "@walletconnect/jsonrpc-utils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
-
- "@walletconnect/randombytes/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
-
- "@walletconnect/socket-transport/ws": ["ws@7.5.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg=="],
-
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, ""],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, ""],
diff --git a/package.json b/package.json
index c8a3fb8..632a1bd 100644
--- a/package.json
+++ b/package.json
@@ -31,9 +31,7 @@
"@angular/material": "^20.2.10",
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
- "@perawallet/connect": "^1.4.2",
"@tailwindcss/postcss": "^4.1.17",
- "algosdk": "^3.5.2",
"multiformats": "^13.4.1",
"nes.css": "^2.3.0",
"pixelarticons": "^1.8.1",
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index 681be9f..8bedf7e 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -1,18 +1,22 @@
-import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
-import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
+import { ApplicationConfig, inject, provideAppInitializer, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
-import { provideHttpClient } from '@angular/common/http';
+import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { routes } from './app.routes';
+import { authInterceptor } from './interceptors/auth.interceptor';
+import { AuthService } from './services/general/auth.service';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
- provideRouter(routes),
+ provideRouter(routes),
provideClientHydration(withEventReplay()),
- provideHttpClient(),
- provideAnimationsAsync()
+ provideHttpClient(withInterceptors([authInterceptor])),
+ provideAppInitializer(() => {
+ const auth = inject(AuthService);
+ return auth.init();
+ }),
]
};
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 9390968..67c13fc 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -1,6 +1,7 @@
import { Routes } from '@angular/router';
-import { StaticLanding } from './components/static-landing/static-landing';
import { MainViewComponent } from './components/main-view/main-view.component';
+import { StaticLanding } from './components/static-landing/static-landing';
+import { CallbackComponent } from './features/auth/callback/callback.component';
export const routes: Routes = [
{
@@ -8,6 +9,10 @@ export const routes: Routes = [
// component: StaticLanding
component: MainViewComponent
},
+ {
+ path: 'auth/callback',
+ component: CallbackComponent
+ },
{
path: 'old-landing',
component: StaticLanding
diff --git a/src/app/components/login-prompt/login-prompt.component.html b/src/app/components/login-prompt/login-prompt.component.html
deleted file mode 100644
index b517d81..0000000
--- a/src/app/components/login-prompt/login-prompt.component.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
Welcome
-
Please sign in to continue
-
-
- @if (currentEnvironment === 'development') {
-
- }
-
-
-
-
-
\ No newline at end of file
diff --git a/src/app/components/login-prompt/login-prompt.component.scss b/src/app/components/login-prompt/login-prompt.component.scss
deleted file mode 100644
index d928b56..0000000
--- a/src/app/components/login-prompt/login-prompt.component.scss
+++ /dev/null
@@ -1,38 +0,0 @@
-.login-container {
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- width: 100%;
-}
-
-.login-card {
- width: 100%;
- max-width: 400px;
- padding: 2rem;
- background-color: var(--theme-section-bg);
-}
-
-.login-title {
- font-weight: bold;
- font-size: 1.5rem !important;
- color: var(--theme-primary-text);
-}
-
-.login-subtitle {
- color: var(--theme-secondary-text);
- font-size: 1.20rem !important;
-}
-
-.card-content {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.login-btn {
- width: 100%;
- margin-bottom: 1rem;
- font-size: 0.8rem;
- font-weight: 500;
-}
diff --git a/src/app/components/login-prompt/login-prompt.component.ts b/src/app/components/login-prompt/login-prompt.component.ts
deleted file mode 100644
index 5503700..0000000
--- a/src/app/components/login-prompt/login-prompt.component.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { CommonModule } from '@angular/common';
-import { ChangeDetectionStrategy, Component, input, OnInit, output, signal } from '@angular/core';
-import { PeraWalletConnect } from '@perawallet/connect';
-import { environment } from '../../../environments/environment.local';
-
-@Component({
- selector: 'app-login-prompt',
- imports: [
- CommonModule
- ],
- templateUrl: './login-prompt.component.html',
- styleUrl: './login-prompt.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush
-})
-export class LoginPromptComponent implements OnInit {
- currentEnvironment: string = environment.environment_name;
- peraInstance = input.required();
- peraWallet!: PeraWalletConnect;
-
- // loginSuccess = output();
- loginError = signal(null);
- userAccountAddress = output();
-
- ngOnInit(): void {
- // Call the peraInstance to get the instance from the input signal, should always use ngOnInit cause
- // input signals are not available in the constructor
- this.peraWallet = this.peraInstance();
- }
-
- reconnectSession(): void {
- this.peraWallet.reconnectSession().then(accounts => {
- if (accounts.length > 0) {
- this.userAccountAddress.emit(accounts[0]);
- } else {
- this.userAccountAddress.emit(null);
- this.loginError.set('Failed to reconnect to Pera Wallet. Please try again.');
- }
- })
- }
-
- onSubmit(): void {
- this.loginError.set(null);
-
- // Check if already connected, try to reconnect first
- if (this.peraWallet.isConnected) {
- this.reconnectSession();
- return;
- }
-
- // Use pera connect to authenticate
- this.peraWallet.connect().then(accounts => {
- if (accounts.length == 0) {
- this.loginError.set('No accounts found. Please connect your Pera Wallet.');
- return;
- }
-
- this.userAccountAddress.emit(accounts[0]);
- }).catch(error => {
- // Handle "Session currently connected" error by trying to reconnect
- if (error?.message?.includes('Session currently connected')) {
- this.reconnectSession();
- return;
- }
- console.error('Error connecting to Pera Wallet:', error);
- this.loginError.set('Failed to connect to Pera Wallet. Please try again.');
- });
- }
-
- // Temporary method to bypass login for development purposes
- onBypassLogin(): void {
- const development = environment.development_wallet;
- this.userAccountAddress.emit(development);
- }
-}
diff --git a/src/app/components/main-view/main-view.component.html b/src/app/components/main-view/main-view.component.html
index 0d7dfa3..5a924dc 100644
--- a/src/app/components/main-view/main-view.component.html
+++ b/src/app/components/main-view/main-view.component.html
@@ -42,7 +42,5 @@
-
-
diff --git a/src/app/components/main-view/main-view.component.ts b/src/app/components/main-view/main-view.component.ts
index 56a55ee..2313c0f 100644
--- a/src/app/components/main-view/main-view.component.ts
+++ b/src/app/components/main-view/main-view.component.ts
@@ -1,22 +1,22 @@
import { CommonModule } from '@angular/common';
-import { Component, ComponentRef, ViewChild, ViewContainerRef, inject, signal } from '@angular/core';
-import { PeraWalletConnect } from '@perawallet/connect';
+import { AfterViewInit, Component, ComponentRef, ViewChild, ViewContainerRef, effect, inject, signal } from '@angular/core';
import { WindowTypes } from '../../enums/window-types.enum';
-import { AlgorandChainIDs, PeraWalletConnectOptions } from '../../interfaces/pera-wallet-connect-options';
-import { LoginOverlayComponent } from '../overlays/login-overlay/login-overlay.component';
+import { AssetService } from '../../services/asset.service';
+import { AuthService } from '../../services/general/auth.service';
+import { SoundEffectService } from '../../services/general/sound-effect.service';
+import { UtilsService } from '../../services/general/utils.service';
import { PixelIconComponent } from '../shared/pixel-icon/pixel-icon.component';
import { AboutWindowComponent } from '../windows/about-window/about-window.component';
import { BreakoutWindowComponent } from '../windows/breakout-window/breakout-window.component';
import { FloatWindow } from '../windows/float-window/float-window.component';
-import { GalleryWindowComponent } from '../windows/gallery-window/gallery-window.component';
+import { GalleryStorageObj, GalleryWindowComponent } from '../windows/gallery-window/gallery-window.component';
import { LaunchpadWindowComponent } from '../windows/launchpad-window/launchpad-window.component';
import { MonoWindowComponent } from '../windows/mono-window/mono-window.component';
import { NotepadWindowComponent } from '../windows/notepad-window/notepad-window.component';
import { RoadmapWindowComponent } from '../windows/roadmap-window/roadmap-window.component';
-import { SettingsWindowComponent } from '../windows/settings-window/settings-window.component';
+import { SettingsStorageObj, SettingsWindowComponent } from '../windows/settings-window/settings-window.component';
import { StyleGuideWindowComponent } from '../windows/style-guide-window/style-guide-window.component';
import { TetrisWindowComponent } from '../windows/tetris-window/tetris-window.component';
-import { AssetService } from '../../services/asset.service';
interface DockItem {
type: WindowTypes;
@@ -25,17 +25,17 @@ interface DockItem {
label: string;
}
+const PENDING_WINDOWS_KEY = 'openedWindowsBeforeAuth';
+
@Component({
selector: 'app-main-view',
templateUrl: 'main-view.component.html',
styleUrls: ['main-view.component.scss'],
imports: [PixelIconComponent, CommonModule]
})
-export class MainViewComponent {
- // 1. Get a reference to the template element where we will host our dynamic components.
+export class MainViewComponent implements AfterViewInit {
@ViewChild('windowHost', { read: ViewContainerRef, static: false }) windowHost!: ViewContainerRef;
@ViewChild('launchpadDrawerHost', { read: ViewContainerRef, static: false }) launchpadDrawerHost!: ViewContainerRef;
- @ViewChild('loginOverlayHost', { read: ViewContainerRef, static: false }) loginOverlayHost!: ViewContainerRef;
openedWindows: ComponentRef[] = [];
@@ -43,12 +43,12 @@ export class MainViewComponent {
launchpadDrawerOpen = signal(false);
launchpadDrawerRef: ComponentRef | null = null;
- // Login overlay state
- loginOverlayRef: ComponentRef | null = null;
-
// Settings window state
settingsWindowRef: ComponentRef | null = null;
+ // Gallery in-memory cache (avoids localStorage quota issues with base64 images)
+ private galleryCache: GalleryStorageObj | null = null;
+
// Dock items - starts with only LaunchPad and Settings
dockItems = signal([
{ type: WindowTypes.LAUNCHPAD, icon: 'apps', label: 'Launch Pad' },
@@ -56,19 +56,13 @@ export class MainViewComponent {
]);
iconMap: Record;
-
- // Pera Wallet Connect Setup
- peraWalletConnectOptions: PeraWalletConnectOptions = {
- shouldShowSignTxnToast: true,
- chainId: AlgorandChainIDs.TestNet // Using TestNet for development
- };
-
- peraWalletConnect: PeraWalletConnect = new PeraWalletConnect(this.peraWalletConnectOptions);
+
+ userImageB64 = signal("");
userAccountAddress = signal(null);
- isAuthenticated = signal(false);
- // End Pera Wallet Connect Setup
assetService: AssetService = inject(AssetService);
+ private authService: AuthService = inject(AuthService);
+ private soundEffectService: SoundEffectService = inject(SoundEffectService);
constructor() {
this.iconMap = {
@@ -98,59 +92,86 @@ export class MainViewComponent {
[WindowTypes.ROADMAP]: { icon: 'map', label: 'Roadmap' },
[WindowTypes.MONO]: { icon: 'work_outline', label: 'Mono', imgIcon: 'icons/mono-icon.png' },
};
+
+ this.fetchSettingsFromLocalStorage();
+
+ // Subscribe to auth service user signal as single source of truth for auth state
+ effect(() => {
+ this.onLoginSuccess(this.authService.user()?.walletAddress ?? null);
+ });
+ }
+
+ ngAfterViewInit(): void {
+ this.restoreSavedWindows();
+ }
+
+ fetchSettingsFromLocalStorage() {
+ // Volume is wallet-independent — restore immediately on load
+ const settings = UtilsService.recoverObjFromLocalStorage('settingsWindowData');
+ if (settings?.volume != null) {
+ this.soundEffectService.setVolume(settings.volume);
+ }
}
onLoginSuccess(accountAddress: string | null): void {
- if (!accountAddress) {
- this.isAuthenticated.set(false);
- this.userAccountAddress.set(null);
-
- // Update settings window if open
- if (this.settingsWindowRef) {
- this.settingsWindowRef.setInput('isAuthenticated', false);
- this.settingsWindowRef.setInput('userAccountAddress', null);
+ if (accountAddress) {
+ const saved = UtilsService.recoverObjFromLocalStorage('settingsWindowData');
+ if (saved?.walletAddress === accountAddress) {
+ // Same wallet — restore profile picture
+ this.userImageB64.set(saved.userImageB64);
+ } else {
+ // Different wallet — discard old settings
+ this.userImageB64.set('');
+ localStorage.removeItem('settingsWindowData');
}
- return;
+
+ // Persist settings tied to this wallet
+ UtilsService.saveObjToLocalStorage('settingsWindowData', {
+ walletAddress: accountAddress,
+ userImageB64: this.userImageB64(),
+ volume: this.soundEffectService.volume(),
+ });
+ } else {
+ // Logout — reset in-memory state but keep localStorage intact for next login
+ this.userImageB64.set('');
}
- this.isAuthenticated.set(true);
this.userAccountAddress.set(accountAddress);
- // Update settings window if open
+ // Update settings window if its already open
if (this.settingsWindowRef) {
- this.settingsWindowRef.setInput('isAuthenticated', true);
this.settingsWindowRef.setInput('userAccountAddress', accountAddress);
+ this.settingsWindowRef.setInput('userImageB64Input', this.userImageB64());
}
}
handleDisconnectWallet(): void {
- this.userAccountAddress.set(null);
- this.isAuthenticated.set(false);
-
- // Update settings window inputs if it's open
- if (this.settingsWindowRef) {
- this.settingsWindowRef.setInput('isAuthenticated', false);
- this.settingsWindowRef.setInput('userAccountAddress', null);
- }
+ this.authService.logout();
+ // State reset will propagate back via authService.user() signal
}
- // MARK: - Window Management
- openWindow() {
- // For a real desktop, you'd want to manage multiple windows.
+ private saveOpenedWindowsToLocalStorage(): void {
+ const types = this.openedWindows
+ .map(ref => this.getWindowType(ref))
+ .filter((t): t is WindowTypes => t !== null);
+ localStorage.setItem(PENDING_WINDOWS_KEY, JSON.stringify(types));
+ }
- // 2. Create an instance of the component you want to show.
- const componentRef = this.windowHost.createComponent(FloatWindow);
+ private restoreSavedWindows(): void {
+ const saved = localStorage.getItem(PENDING_WINDOWS_KEY);
+ if (!saved) return;
- // 3. Subscribe to the close event to destroy the component when requested.
- const closeSub = componentRef.instance.closeEvent.subscribe(() => {
- this.closeWindow(componentRef);
- closeSub.unsubscribe();
- });
+ localStorage.removeItem(PENDING_WINDOWS_KEY);
- // 4. Keep track of the created component.
- this.openedWindows.push(componentRef);
+ try {
+ const types: WindowTypes[] = JSON.parse(saved);
+ types.forEach(type => this.openWindowByType(type));
+ } catch {
+ // Ignore malformed data
+ }
}
+ // MARK: - Window Management
closeWindow(componentRef: ComponentRef) {
const index = this.openedWindows.indexOf(componentRef);
if (index > -1) {
@@ -159,6 +180,17 @@ export class MainViewComponent {
this.settingsWindowRef = null;
}
+ // Snapshot gallery state before destroying so it can be restored on reopen
+ if (componentRef.instance instanceof GalleryWindowComponent) {
+ const g = componentRef.instance;
+ this.galleryCache = {
+ scrollPosition: 0,
+ corvidNfts: g.corvidNfts(),
+ page: g.nextPage(),
+ isLastPage: g.isLastPage(),
+ };
+ }
+
this.openedWindows.splice(index, 1);
componentRef.destroy();
@@ -168,7 +200,6 @@ export class MainViewComponent {
}
private updateDockItems() {
- const currentDockItems = this.dockItems();
const updatedDockItems: DockItem[] = [
{ type: WindowTypes.LAUNCHPAD, icon: 'apps', label: 'Launch Pad' },
{ type: WindowTypes.SETTINGS, icon: 'settings', label: 'Settings' }
@@ -199,7 +230,6 @@ export class MainViewComponent {
if (componentRef.instance instanceof MonoWindowComponent) return WindowTypes.MONO;
// LAUNCHPAD is now a drawer, not a window
if (componentRef.instance instanceof StyleGuideWindowComponent) return WindowTypes.STYLE_GUIDE;
- // LOGIN is now an overlay, not a window
return null;
}
@@ -214,6 +244,11 @@ export class MainViewComponent {
const componentRef = existingComponentRef ?? this.windowHost.createComponent(component);
+ // If its the Gallery Window, we restore its state from the cache from the main view
+ if (componentRef.instance instanceof GalleryWindowComponent && this.galleryCache) {
+ componentRef.setInput('cachedData', this.galleryCache);
+ }
+
// If its an existing window, just bring it to front
if (existingComponentRef) {
componentRef.instance.bringWindowToFront();
@@ -261,7 +296,6 @@ export class MainViewComponent {
}
openWindowByType(type: string) {
- // TODO: Check if I should hide the dock intead when the drawer is openned
// If launchpad drawer opened, close it
this.closeLaunchpadDrawer();
@@ -285,7 +319,10 @@ export class MainViewComponent {
this.openOrCreateWindowAdvanced(StyleGuideWindowComponent);
break;
case WindowTypes.LOGIN:
- this.showLoginOverlay();
+ this.saveOpenedWindowsToLocalStorage();
+ this.authService.startVerification().catch(error => {
+ console.error('Error starting verification:', error);
+ });
break;
case WindowTypes.BREAKOUT:
this.openOrCreateWindowAdvanced(BreakoutWindowComponent);
@@ -299,9 +336,6 @@ export class MainViewComponent {
case WindowTypes.MONO:
this.openOrCreateWindowAdvanced(MonoWindowComponent);
break;
- // case WindowTypes.SOUNDCLOUD_PLAYER: // NOTE: Removed till a new music api is implemented
- // this.openOrCreateWindowAdvanced(PlayerWindowComponent);
- // break;
}
}
@@ -362,12 +396,8 @@ export class MainViewComponent {
// Store reference for later updates
this.settingsWindowRef = settingsRef;
- // Set inputs
- settingsRef.setInput('peraInstance', this.peraWalletConnect);
-
- // Pass login state to settings window
- settingsRef.setInput('isAuthenticated', this.isAuthenticated());
settingsRef.setInput('userAccountAddress', this.userAccountAddress());
+ settingsRef.setInput('userImageB64Input', this.userImageB64());
// Handle Change User button from Settings
settingsRef.instance.closeEvent.subscribe((windowType: any) => {
@@ -381,52 +411,4 @@ export class MainViewComponent {
this.handleDisconnectWallet();
});
}
-
- // MARK: - Login Overlay Management
- showLoginOverlay() {
- // Check for existing instance (prevent duplicates)
- if (this.loginOverlayRef) {
- this.loginOverlayRef.instance.isOpen.set(true);
- return;
- }
-
- // Create the overlay component
- this.loginOverlayRef = this.loginOverlayHost.createComponent(LoginOverlayComponent);
-
- // Set inputs
- this.loginOverlayRef.setInput('peraInstance', this.peraWalletConnect);
-
- // Subscribe to login success
- this.loginOverlayRef.instance.loginSuccess.subscribe((address: string) => {
- this.onLoginSuccess(address);
- });
-
- // Subscribe to close events
- this.loginOverlayRef.instance.closeEvent.subscribe(() => {
- this.closeLoginOverlay();
- });
-
- // Open the overlay with a small delay to trigger animation
- setTimeout(() => {
- if (this.loginOverlayRef) {
- this.loginOverlayRef.instance.isOpen.set(true);
- }
- }, 10);
- }
-
- closeLoginOverlay() {
- if (this.loginOverlayRef) {
- // Set isOpen to false to trigger fade-out animation
- this.loginOverlayRef.instance.isOpen.set(false);
-
- // Destroy the component after animation completes (300ms)
- setTimeout(() => {
- if (this.loginOverlayRef) {
- this.loginOverlayRef.destroy();
- this.loginOverlayRef = null;
- }
- }, 300);
- }
- }
-
-}
\ No newline at end of file
+}
diff --git a/src/app/components/nft-card/nft-card.component.html b/src/app/components/nft-card/nft-card.component.html
index 1453b79..9e56dde 100644
--- a/src/app/components/nft-card/nft-card.component.html
+++ b/src/app/components/nft-card/nft-card.component.html
@@ -1,4 +1,4 @@
-
![{{ title() }}]()
+
{{ title() }}
\ No newline at end of file
diff --git a/src/app/components/nft-card/nft-card.component.ts b/src/app/components/nft-card/nft-card.component.ts
index 03c34ac..9c01488 100644
--- a/src/app/components/nft-card/nft-card.component.ts
+++ b/src/app/components/nft-card/nft-card.component.ts
@@ -8,6 +8,7 @@ export class NftCardComponent implements OnInit {
imageUrl = input('');
title = input('');
description = input('');
+ imageB64 = input('');
constructor() {}
ngOnInit() {}
diff --git a/src/app/components/overlays/login-overlay/login-overlay.component.html b/src/app/components/overlays/login-overlay/login-overlay.component.html
deleted file mode 100644
index 9e97fb6..0000000
--- a/src/app/components/overlays/login-overlay/login-overlay.component.html
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @if (loginError()) {
-
- }
-
-
-
- @if (currentEnvironment === 'development') {
-
- }
-
-
-
-
-
-
-
-
diff --git a/src/app/components/overlays/login-overlay/login-overlay.component.scss b/src/app/components/overlays/login-overlay/login-overlay.component.scss
deleted file mode 100644
index 55eaea6..0000000
--- a/src/app/components/overlays/login-overlay/login-overlay.component.scss
+++ /dev/null
@@ -1,181 +0,0 @@
-@use '../../../../themes/nes-pixel-mixins' as nes-pixel-mixins;
-
-// ============================================
-// Login Overlay Container
-// ============================================
-.login-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 10500;
- display: flex;
- align-items: center;
- justify-content: center;
- pointer-events: none;
-
- &.overlay-visible {
- pointer-events: auto;
- }
-}
-
-// ============================================
-// Login Backdrop
-// ============================================
-.login-backdrop {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: var(--theme-primary-bg);
- backdrop-filter: blur(8px);
- z-index: 10500;
-
- opacity: 0;
- transition: opacity 0.3s ease;
-
- &.backdrop-visible {
- opacity: 1;
- }
-}
-
-// ============================================
-// Login Card
-// ============================================
-.login-card {
- position: relative;
- z-index: 11000;
- width: 90%;
- max-width: 450px;
- padding: 2rem;
-
- background-color: var(--theme-secondary-bg, var(--mat-sys-surface));
- border: 4px solid var(--theme-border-color, var(--mat-sys-outline-variant));
-
- @include nes-pixel-mixins.pixel-border(var(--theme-border-color));
-
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
-
- transform: scale(0.9) translateY(-20px);
- opacity: 0;
- transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
-
- &.card-visible {
- transform: scale(1) translateY(0);
- opacity: 1;
- }
-}
-
-// ============================================
-// Login Header
-// ============================================
-.login-header {
- text-align: center;
- margin-bottom: 2rem;
-}
-
-.login-title {
- font-family: 'Press Start 2P', cursive;
- font-size: 1.75rem;
- color: var(--theme-primary-text, var(--mat-sys-on-surface));
- margin: 0 0 1rem 0;
- text-shadow: 2px 2px 0 var(--theme-shadow-color, rgba(0,0,0,0.3));
-}
-
-.login-subtitle {
- font-size: 0.875rem;
- color: var(--theme-secondary-text, var(--mat-sys-on-surface-variant));
- margin: 0;
- line-height: 1.6;
-}
-
-// ============================================
-// Error Message
-// ============================================
-.error-message {
- margin-bottom: 1.5rem;
- padding: 1rem;
- font-size: 0.75rem;
- line-height: 1.5;
-
- p {
- margin: 0;
- }
-}
-
-// ============================================
-// Login Form
-// ============================================
-.login-form {
- display: flex;
- flex-direction: column;
- gap: 1rem;
- margin-bottom: 1.5rem;
-
- button {
- width: 100%;
- font-size: 0.75rem;
- padding: 1rem;
- transition: all 0.2s ease;
-
- &:hover:not(:disabled) {
- transform: translateY(-2px);
- box-shadow: 0 4px 8px var(--theme-shadow-color, rgba(0,0,0,0.3));
- }
-
- &:active:not(:disabled) {
- transform: translateY(0);
- }
-
- &:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- }
-}
-
-// ============================================
-// Login Footer
-// ============================================
-.login-footer {
- text-align: center;
- padding-top: 1rem;
- border-top: 2px solid var(--theme-border-color, var(--mat-sys-outline-variant));
-}
-
-.login-footer-text {
- font-size: 0.65rem;
- color: var(--theme-secondary-text, var(--mat-sys-on-surface-variant));
- margin: 0;
- opacity: 0.7;
-}
-
-// ============================================
-// Responsive Design
-// ============================================
-@media (max-width: 640px) {
- .login-card {
- width: 95%;
- padding: 1.5rem;
- }
-
- .login-title {
- font-size: 1.25rem;
- }
-
- .login-subtitle {
- font-size: 0.75rem;
- }
-}
-
-// ============================================
-// Reduced Motion
-// ============================================
-@media (prefers-reduced-motion: reduce) {
- .login-backdrop,
- .login-card {
- transition: none;
- }
-}
diff --git a/src/app/components/overlays/login-overlay/login-overlay.component.ts b/src/app/components/overlays/login-overlay/login-overlay.component.ts
deleted file mode 100644
index 91ff783..0000000
--- a/src/app/components/overlays/login-overlay/login-overlay.component.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { Component, input, signal, output, ChangeDetectionStrategy, OnInit, HostListener } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { PeraWalletConnect } from '@perawallet/connect';
-import { environment } from '../../../../environments/environment.local';
-
-@Component({
- selector: 'app-login-overlay',
- imports: [CommonModule],
- templateUrl: './login-overlay.component.html',
- styleUrls: ['./login-overlay.component.scss'],
- changeDetection: ChangeDetectionStrategy.OnPush
-})
-export class LoginOverlayComponent implements OnInit {
- // Inputs
- peraInstance = input.required();
-
- // Outputs
- closeEvent = output();
- loginSuccess = output();
-
- // State
- isOpen = signal(false);
- loginError = signal(null);
- isLoading = signal(false);
-
- // Environment
- currentEnvironment: string = environment.environment_name;
-
- // Pera instance
- peraWallet!: PeraWalletConnect;
-
- ngOnInit(): void {
- this.peraWallet = this.peraInstance();
- }
-
- @HostListener('document:keydown.escape')
- onEscapeKey() {
- if (this.isOpen()) {
- this.close();
- }
- }
-
- onSubmit(): void {
- this.loginError.set(null);
- this.isLoading.set(true);
-
- this.peraWallet.connect().then(accounts => {
- this.isLoading.set(false);
-
- if (accounts.length === 0) {
- this.loginError.set('No accounts found. Please connect your Pera Wallet.');
- return;
- }
-
- this.loginSuccess.emit(accounts[0]);
- this.close();
- }).catch(error => {
- console.error('Error connecting to Pera Wallet:', error);
- this.isLoading.set(false);
- this.loginError.set('Failed to connect to Pera Wallet. Please try again.');
- });
- }
-
- onBypassLogin(): void {
- const development = environment.development_wallet;
- this.loginSuccess.emit(development);
- this.close();
- }
-
- close(): void {
- this.isOpen.set(false);
- // Emit close event after animation completes
- setTimeout(() => {
- this.closeEvent.emit();
- }, 300);
- }
-}
diff --git a/src/app/components/slider/slider.component.ts b/src/app/components/slider/slider.component.ts
index 4eb08d3..f77f47e 100644
--- a/src/app/components/slider/slider.component.ts
+++ b/src/app/components/slider/slider.component.ts
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
-import { Component, OnInit, InputSignal, input, OutputEmitterRef, output, WritableSignal, signal, ChangeDetectionStrategy } from '@angular/core';
+import { ChangeDetectionStrategy, Component, InputSignal, OnInit, OutputEmitterRef, WritableSignal, input, output, signal } from '@angular/core';
import { PixelIconComponent } from '../shared/pixel-icon/pixel-icon.component';
@Component({
diff --git a/src/app/components/soundcloud-player/soundcloud-player.component.ts b/src/app/components/soundcloud-player/soundcloud-player.component.ts
index 08fab88..a6badb0 100644
--- a/src/app/components/soundcloud-player/soundcloud-player.component.ts
+++ b/src/app/components/soundcloud-player/soundcloud-player.component.ts
@@ -1,8 +1,7 @@
import { CommonModule } from '@angular/common';
-import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
+import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { SoundcloudService } from '../../services/soundcloud.service';
-import { environment } from '../../../environments/environment.local';
interface Track {
title: string;
@@ -28,12 +27,9 @@ export class SoundcloudPlayerComponent implements OnInit {
currentTime = 0; // in seconds
private progressInterval: any;
-
- constructor(
- private soundcloudService: SoundcloudService
- ) {
+ private soundcloudService = inject(SoundcloudService);
- }
+ constructor() { }
ngOnInit(): void {
// Placeholder for loading a track
diff --git a/src/app/components/windows/about-window/about-window.component.ts b/src/app/components/windows/about-window/about-window.component.ts
index 2d1b692..9593565 100644
--- a/src/app/components/windows/about-window/about-window.component.ts
+++ b/src/app/components/windows/about-window/about-window.component.ts
@@ -2,10 +2,9 @@ import { CommonModule } from '@angular/common';
import { AfterViewInit, Component, effect, ElementRef, inject, input, OnInit, viewChild } from '@angular/core';
import { DraggableDirective } from '../../../directives/draggable.directive';
import { ResizableDirective } from '../../../directives/resizable.directive';
-import { NftCardComponent } from '../../nft-card/nft-card.component';
-import { FloatWindow } from '../float-window/float-window.component';
import { ThemeService } from '../../../services/general/theme.service';
import { NavbarComponent } from '../../navbar/navbar.component';
+import { FloatWindow } from '../float-window/float-window.component';
declare const Chart: any;
diff --git a/src/app/components/windows/breakout-window/breakout-window.component.ts b/src/app/components/windows/breakout-window/breakout-window.component.ts
index 174f86d..fc23ee7 100644
--- a/src/app/components/windows/breakout-window/breakout-window.component.ts
+++ b/src/app/components/windows/breakout-window/breakout-window.component.ts
@@ -1,8 +1,8 @@
-import { Component, signal, ChangeDetectionStrategy, HostListener, OnDestroy, AfterViewInit, ElementRef, ViewChild, input } from '@angular/core';
-import { FloatWindow } from '../float-window/float-window.component';
-import { DraggableDirective } from '../../../directives/draggable.directive';
import { CommonModule } from '@angular/common';
+import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, HostListener, OnDestroy, ViewChild, input, signal } from '@angular/core';
+import { DraggableDirective } from '../../../directives/draggable.directive';
import { PixelIconComponent } from '../../shared/pixel-icon/pixel-icon.component';
+import { FloatWindow } from '../float-window/float-window.component';
type GameState = 'ready' | 'playing' | 'paused' | 'won' | 'lost';
diff --git a/src/app/components/windows/float-window/float-window.component.ts b/src/app/components/windows/float-window/float-window.component.ts
index 9c18014..d4dc2e6 100644
--- a/src/app/components/windows/float-window/float-window.component.ts
+++ b/src/app/components/windows/float-window/float-window.component.ts
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
-import { Component, ViewChild, signal, effect, input, output } from '@angular/core';
+import { Component, ViewChild, effect, input, output, signal } from '@angular/core';
import { DraggableDirective } from '../../../directives/draggable.directive';
import { ResizableDirective, ResizeEvent } from '../../../directives/resizable.directive';
diff --git a/src/app/components/windows/gallery-window/gallery-window.component.html b/src/app/components/windows/gallery-window/gallery-window.component.html
index fdd156e..4e1ca6e 100644
--- a/src/app/components/windows/gallery-window/gallery-window.component.html
+++ b/src/app/components/windows/gallery-window/gallery-window.component.html
@@ -4,10 +4,6 @@
{{ title() }}
@@ -37,7 +33,7 @@
{{ title() }}
} @else {
@for (asset of corvidNfts(); track asset.name) {
}
diff --git a/src/app/components/windows/gallery-window/gallery-window.component.ts b/src/app/components/windows/gallery-window/gallery-window.component.ts
index 59edc43..d79a29c 100644
--- a/src/app/components/windows/gallery-window/gallery-window.component.ts
+++ b/src/app/components/windows/gallery-window/gallery-window.component.ts
@@ -1,4 +1,5 @@
-import { ChangeDetectionStrategy, Component, ElementRef, inject, OnDestroy, OnInit, signal, ViewChild, input } from '@angular/core';
+import { ChangeDetectionStrategy, Component, ElementRef, inject, input, OnDestroy, OnInit, signal, ViewChild } from '@angular/core';
+import { MatTooltipModule } from '@angular/material/tooltip';
import { debounceTime, Subject, Subscription } from 'rxjs';
import { DraggableDirective } from '../../../directives/draggable.directive';
import { ResizableDirective } from '../../../directives/resizable.directive';
@@ -7,20 +8,17 @@ import { AssetService } from '../../../services/asset.service';
import { NftCardComponent } from "../../nft-card/nft-card.component";
import { SkeletonCardComponent } from "../../skeleton-card/skeleton-card.component";
import { FloatWindow } from '../float-window/float-window.component';
-import { UtilsService } from '../../../services/general/utils.service';
-import { MatTooltipModule } from '@angular/material/tooltip';
-import { PixelIconComponent } from '../../shared/pixel-icon/pixel-icon.component';
export interface GalleryStorageObj {
scrollPosition: number;
corvidNfts: CorvidNft[] | null;
+ page: number;
isLastPage: boolean;
- nextToken: string | null;
}
@Component({
selector: 'app-gallery-window',
- imports: [DraggableDirective, ResizableDirective, NftCardComponent, SkeletonCardComponent, MatTooltipModule, PixelIconComponent],
+ imports: [DraggableDirective, ResizableDirective, NftCardComponent, SkeletonCardComponent, MatTooltipModule],
templateUrl: 'gallery-window.component.html',
styleUrls: ['gallery-window.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -29,6 +27,7 @@ export class GalleryWindowComponent extends FloatWindow implements OnInit, OnDes
@ViewChild('scrollableElement') scrollableElement!: ElementRef
;
override title = input('Gallery');
+ cachedData = input(null);
private assetService = inject(AssetService);
private scrollSubject = new Subject();
@@ -40,8 +39,8 @@ export class GalleryWindowComponent extends FloatWindow implements OnInit, OnDes
isLoadingMore = signal(false);
hasError = signal(false);
+ nextPage = signal(1);
defaultPageSize = 20;
- nextToken: string | null = null;
constructor() {
super();
@@ -53,8 +52,6 @@ export class GalleryWindowComponent extends FloatWindow implements OnInit, OnDes
this.width.set(600);
this.height.set(400);
}
-
- this.initializeGallery();
}
ngOnInit() {
@@ -63,6 +60,8 @@ export class GalleryWindowComponent extends FloatWindow implements OnInit, OnDes
).subscribe(() => {
this.requestItems();
});
+
+ this.initializeGallery();
}
ngOnDestroy(): void {
@@ -75,14 +74,14 @@ export class GalleryWindowComponent extends FloatWindow implements OnInit, OnDes
if (!this.scrollableElement) return;
const element = this.scrollableElement.nativeElement;
- const scrollOffset = element.scrollHeight - 10;
+ const scrollOffset = element.scrollHeight - 20;
if (element.scrollTop + element.clientHeight >= scrollOffset) {
this.scrollSubject.next();
}
}
- requestItems(fallback: boolean = false): void {
+ requestItems(): void {
if (this.isLastPage() || (this.isLoading() || this.isLoadingMore()) && this.corvidNfts().length > 0) return;
const isInitialLoad = this.corvidNfts().length === 0;
@@ -94,113 +93,56 @@ export class GalleryWindowComponent extends FloatWindow implements OnInit, OnDes
this.isLoadingMore.set(true);
}
- this.assetService.listCreatedAssets(this.defaultPageSize, this.nextToken, fallback).subscribe({
- next: (response) => {
- this.isLastPage.set(response.assets.length < this.defaultPageSize || !response.nextToken);
- this.nextToken = response.nextToken;
-
- const nfts = this.assetService.listCorvidNftsFromCreatedAssets(response);
- nfts.subscribe({
- next: (nfts) => {
- this.corvidNfts.update(current => [...current, ...nfts]);
- this.isLoading.set(false);
- this.isLoadingMore.set(false);
-
- this.saveToLocalStorage();
- },
- error: err => {
- console.error('Error fetching NFT metadata:', err);
- this.isLoading.set(false);
- this.isLoadingMore.set(false);
- if (this.corvidNfts().length === 0) {
- this.hasError.set(true);
- }
- }
- });
- },
- error: err => {
- console.error('Error fetching assets:', err);
- this.nextToken = null;
- this.isLoading.set(false);
- this.isLoadingMore.set(false);
+ this.assetService.getCorvidNFTsPaginated(this.nextPage(), this.defaultPageSize).subscribe({
+ next: response => {
+ this.nextPage.update(current => current + 1);
+ this.corvidNfts.update(current => [...current, ...response.items]);
+
if (this.corvidNfts().length === 0) {
this.hasError.set(true);
}
+
+ this.isLastPage.set(response.items.length < this.defaultPageSize);
+ this.isLoading.set(false);
+ this.isLoadingMore.set(false);
+ },
+ error: error => {
+ console.error('Error fetching assets:', error);
+ this.isLoading.set(false);
+ this.isLoadingMore.set(false);
+ this.hasError.set(true);
}
});
}
- /**
- * Get cached gallery window data from local storage
- * @returns True if cache was loaded successfully, false otherwise
- */
- loadFromLocalStorage(): boolean {
- const storedData = UtilsService.recoverObjFromLocalStorage('galleryWindowData');
-
- if (storedData?.corvidNfts && storedData.corvidNfts.length > 0) {
- this.corvidNfts.set(storedData.corvidNfts);
- this.isLastPage.set(storedData.isLastPage);
- this.nextToken = storedData.nextToken;
- this.isLoading.set(false);
- return true;
- }
- return false;
- }
-
- /**
- * Save gallery window data to local storage
- */
- saveToLocalStorage() {
- const data: GalleryStorageObj = {
- scrollPosition: 0,
- corvidNfts: this.corvidNfts(),
- isLastPage: this.isLastPage(),
- nextToken: this.nextToken
- };
-
- UtilsService.saveObjToLocalStorage('galleryWindowData', data);
- }
-
- /**
- * Clear local storage
- */
- clearLocalStorage() {
- UtilsService.clearGalleryCache();
- }
-
retry(): void {
this.hasError.set(false);
- this.nextToken = null;
this.isLastPage.set(false);
-
- this.clearLocalStorage();
this.corvidNfts.set([]);
if (this.scrollableElement) {
this.scrollableElement.nativeElement.scrollTop = 0;
}
- this.requestItems(true);
+ this.requestItems();
}
- /**
- * Initialize gallery - uses cache if available, otherwise fetches from API
- */
initializeGallery(): void {
- const hasCachedData = this.loadFromLocalStorage();
+ const cache = this.cachedData();
- if (!hasCachedData) {
+ if (cache?.corvidNfts && cache.corvidNfts.length > 0) {
+ this.corvidNfts.set(cache.corvidNfts);
+ this.nextPage.set(cache.page);
+ this.isLastPage.set(cache.isLastPage);
+ this.isLoading.set(false);
+ } else {
this.requestItems();
}
}
- /**
- * Force refresh - clears cache and fetches fresh data
- */
refreshGallery(): void {
- UtilsService.clearGalleryCache();
this.corvidNfts.set([]);
- this.nextToken = null;
+ this.nextPage.set(1);
this.isLastPage.set(false);
this.hasError.set(false);
this.requestItems();
diff --git a/src/app/components/windows/launchpad-window/launchpad-window.component.ts b/src/app/components/windows/launchpad-window/launchpad-window.component.ts
index eea937a..8318cdf 100644
--- a/src/app/components/windows/launchpad-window/launchpad-window.component.ts
+++ b/src/app/components/windows/launchpad-window/launchpad-window.component.ts
@@ -1,8 +1,8 @@
-import { Component, input, signal, output, ChangeDetectionStrategy, HostListener } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { PixelIconComponent } from '../../shared/pixel-icon/pixel-icon.component';
-import { WindowTypes } from '../../../enums/window-types.enum';
+import { ChangeDetectionStrategy, Component, HostListener, input, output, signal } from '@angular/core';
import { environment } from '../../../../environments/environment.local';
+import { WindowTypes } from '../../../enums/window-types.enum';
+import { PixelIconComponent } from '../../shared/pixel-icon/pixel-icon.component';
interface AppIcon {
type: WindowTypes;
diff --git a/src/app/components/windows/mono-window/mono-window.component.ts b/src/app/components/windows/mono-window/mono-window.component.ts
index 4e13e16..24b4bd8 100644
--- a/src/app/components/windows/mono-window/mono-window.component.ts
+++ b/src/app/components/windows/mono-window/mono-window.component.ts
@@ -1,9 +1,9 @@
+import { CommonModule } from '@angular/common';
import { Component, input, OnInit, signal, WritableSignal } from '@angular/core';
-import { FloatWindow } from '../float-window/float-window.component';
import { DraggableDirective } from '../../../directives/draggable.directive';
import { ResizableDirective } from '../../../directives/resizable.directive';
-import { CommonModule } from '@angular/common';
import { NavbarComponent, NavbarItem } from "../../navbar/navbar.component";
+import { FloatWindow } from '../float-window/float-window.component';
interface monoAppletData {
id: string;
diff --git a/src/app/components/windows/notepad-window/notepad-window.component.ts b/src/app/components/windows/notepad-window/notepad-window.component.ts
index c8e01ca..329cae2 100644
--- a/src/app/components/windows/notepad-window/notepad-window.component.ts
+++ b/src/app/components/windows/notepad-window/notepad-window.component.ts
@@ -1,14 +1,14 @@
-import { Component, OnInit, signal, computed, input, inject } from '@angular/core';
-import { FloatWindow } from '../float-window/float-window.component';
-import { DraggableDirective } from '../../../directives/draggable.directive';
import { CommonModule } from '@angular/common';
+import { Component, computed, inject, input, OnInit, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
-import { DomSanitizer } from '@angular/platform-browser';
-import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
+import { DomSanitizer } from '@angular/platform-browser';
+import { DraggableDirective } from '../../../directives/draggable.directive';
import { ThemeService } from '../../../services/general/theme.service';
import { PixelIconComponent } from "../../shared/pixel-icon/pixel-icon.component";
+import { FloatWindow } from '../float-window/float-window.component';
@Component({
selector: 'app-notepad-window',
diff --git a/src/app/components/windows/player-window/player-window.component.ts b/src/app/components/windows/player-window/player-window.component.ts
index 11d0421..d6634b2 100644
--- a/src/app/components/windows/player-window/player-window.component.ts
+++ b/src/app/components/windows/player-window/player-window.component.ts
@@ -1,8 +1,8 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit, input } from '@angular/core';
import { DraggableDirective } from '../../../directives/draggable.directive';
-import { FloatWindow } from '../float-window/float-window.component';
import { SoundcloudPlayerComponent } from "../../soundcloud-player/soundcloud-player.component";
+import { FloatWindow } from '../float-window/float-window.component';
@Component({
selector: 'app-player-window',
diff --git a/src/app/components/windows/roadmap-window/roadmap-window.component.ts b/src/app/components/windows/roadmap-window/roadmap-window.component.ts
index 3a0258a..ad3beb7 100644
--- a/src/app/components/windows/roadmap-window/roadmap-window.component.ts
+++ b/src/app/components/windows/roadmap-window/roadmap-window.component.ts
@@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common';
import { Component, computed, input, signal } from '@angular/core';
import { DraggableDirective } from '../../../directives/draggable.directive';
import { ResizableDirective } from '../../../directives/resizable.directive';
-import { FloatWindow } from '../float-window/float-window.component';
-import { PixelIconComponent } from '../../shared/pixel-icon/pixel-icon.component';
import { CvdCheckboxComponent } from "../../shared/corvid-checkbox/corvid-checkbox.component";
+import { PixelIconComponent } from '../../shared/pixel-icon/pixel-icon.component';
+import { FloatWindow } from '../float-window/float-window.component';
interface RoadmapItem {
text: string;
diff --git a/src/app/components/windows/settings-window/settings-window.component.html b/src/app/components/windows/settings-window/settings-window.component.html
index c2c573e..d91fb6d 100644
--- a/src/app/components/windows/settings-window/settings-window.component.html
+++ b/src/app/components/windows/settings-window/settings-window.component.html
@@ -12,21 +12,37 @@ {{ title() }}
+ @if (contentDisplaying() === 'picture-picker') {
+
+
+
+ @for (asset of userAssets(); track asset.name) {
+
+ }
+
+
+ } @else {
+
-
- @if(!isAuthenticated()) {
+
+ @if(!authService.user()) {
} @else {
- @if(!userImage()) {
+ @if(!userImageB64()) {
} @else {
-
![User Profile]()
+
![User Profile]()
}
-
+
@@ -35,7 +51,7 @@
{{ title() }}
- @if(isAuthenticated()) {
+ @if(authService.user()) {
Logged In
@@ -55,7 +71,7 @@
{{ title() }}
Not Logged In
-
+
+
+ }
diff --git a/src/app/components/windows/settings-window/settings-window.component.scss b/src/app/components/windows/settings-window/settings-window.component.scss
index a61a740..dbc5bc7 100644
--- a/src/app/components/windows/settings-window/settings-window.component.scss
+++ b/src/app/components/windows/settings-window/settings-window.component.scss
@@ -225,6 +225,53 @@
}
}
+// Picture Picker Overlay
+.picture-picker-overlay {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.picture-picker-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem 1rem;
+ background-color: var(--theme-primary-bg);
+ border-bottom: 2px solid var(--theme-border-color, #000);
+ flex-shrink: 0;
+
+ h3 {
+ margin: 0;
+ font-family: 'Press Start 2P', cursive;
+ font-size: 0.75rem;
+ color: var(--theme-primary-text);
+ }
+}
+
+.picture-picker-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 0.75rem;
+ padding: 1rem;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.picture-picker-item {
+ cursor: pointer;
+ transition: transform 0.15s ease, opacity 0.15s ease;
+
+ &:hover {
+ transform: scale(1.05);
+ opacity: 0.85;
+ }
+
+ &:active {
+ transform: scale(0.97);
+ }
+}
+
// Volume Control Layout
.volume-control {
flex-wrap: wrap;
diff --git a/src/app/components/windows/settings-window/settings-window.component.ts b/src/app/components/windows/settings-window/settings-window.component.ts
index 10b28c3..68ea751 100644
--- a/src/app/components/windows/settings-window/settings-window.component.ts
+++ b/src/app/components/windows/settings-window/settings-window.component.ts
@@ -1,32 +1,48 @@
import { CommonModule } from '@angular/common';
-import { Component, OnInit, computed, inject, input, output, signal } from '@angular/core';
-import { PeraWalletConnect } from '@perawallet/connect';
+import { Component, computed, effect, inject, input, output, signal } from '@angular/core';
import { DraggableDirective } from '../../../directives/draggable.directive';
+import { ResizableDirective } from '../../../directives/resizable.directive';
import { WindowTypes } from '../../../enums/window-types.enum';
+import { CorvidNft } from '../../../interfaces/corvid-nft.interface';
+import { AssetService } from '../../../services/asset.service';
+import { AuthService } from '../../../services/general/auth.service';
import { SoundEffectService } from '../../../services/general/sound-effect.service';
+import { UtilsService } from '../../../services/general/utils.service';
+import { NftCardComponent } from '../../nft-card/nft-card.component';
import { PixelIconComponent } from '../../shared/pixel-icon/pixel-icon.component';
+import { CvdSliderComponent } from "../../slider/slider.component";
import { ThemeSwitcherComponent } from "../../theme-switcher/theme-switcher.component";
import { FloatWindow } from '../float-window/float-window.component';
-import { ResizableDirective } from '../../../directives/resizable.directive';
-import { CvdSliderComponent } from "../../slider/slider.component";
+
+export interface SettingsStorageObj {
+ walletAddress: string | null;
+ userImageB64: string;
+ volume: number;
+}
@Component({
selector: 'app-settings-window',
- imports: [CommonModule, ResizableDirective, DraggableDirective, ThemeSwitcherComponent, PixelIconComponent, CvdSliderComponent],
+ imports: [CommonModule, ResizableDirective, DraggableDirective, ThemeSwitcherComponent, PixelIconComponent, CvdSliderComponent, NftCardComponent],
templateUrl: 'settings-window.component.html',
styleUrls: ['settings-window.component.scss']
})
-export class SettingsWindowComponent extends FloatWindow implements OnInit {
+export class SettingsWindowComponent extends FloatWindow {
override title = input
('Settings');
- peraInstance = input.required();
- userImage = signal("");
- isAuthenticated = input(false);
+ userImageB64Input = input('');
+ userImageB64 = signal('');
userAccountAddress = input(null);
+ userAssets = signal([]);
+ contentDisplaying = signal<'settings' | 'picture-picker'>('settings');
+
logoutRequested = output();
+ profilePictureSelected = output();
+ // Services Injection
soundEffectService = inject(SoundEffectService);
+ assetService = inject(AssetService);
+ protected authService = inject(AuthService);
// Volume icon based on current volume level
volumeIcon = computed(() => {
@@ -40,12 +56,9 @@ export class SettingsWindowComponent extends FloatWindow implements OnInit {
// Math reference for template
protected readonly Math = Math;
- // Pera instance
- peraWallet!: PeraWalletConnect;
-
constructor() {
super();
-
+
if (window.innerWidth >= 1920) {
this.width.set(600);
this.height.set(830);
@@ -53,10 +66,32 @@ export class SettingsWindowComponent extends FloatWindow implements OnInit {
this.width.set(600);
this.height.set(400);
}
+
+ // Restore volume from saved settings (wallet-independent)
+ const saved = UtilsService.recoverObjFromLocalStorage('settingsWindowData');
+ if (saved?.volume != null) {
+ this.soundEffectService.setVolume(saved.volume);
+ }
+
+ effect(() => {
+ this.onUserAccountAddressChange(this.userAccountAddress());
+ });
+
+ effect(() => {
+ const incoming = this.userImageB64Input();
+ if (incoming !== this.userImageB64()) {
+ this.userImageB64.set(incoming);
+ }
+ });
}
- ngOnInit() {
- this.peraWallet = this.peraInstance();
+ onUserAccountAddressChange(address: string | null): void {
+ if (!address) return;
+
+ this.assetService.getWalletAssetsHolding(address).subscribe(assets => {
+ console.log(assets);
+ this.userAssets.set(assets);
+ });
}
onChangeUser() {
@@ -65,29 +100,37 @@ export class SettingsWindowComponent extends FloatWindow implements OnInit {
}
onChangeProfilePicture() {
- if (!this.isAuthenticated()) return;
-
- // Placeholder for future profile picture gallery/upload
- console.log('Profile picture change requested');
+ if (!this.authService.user()) return;
+ this.contentDisplaying.set('picture-picker');
}
- onLogout() {
- this.handleDisconnectWallet();
- this.logoutRequested.emit();
+ onAssetPicked(asset: CorvidNft) {
+ this.userImageB64.set(asset.imageB64 ?? '');
+ this.contentDisplaying.set('settings');
+ this.updateSettingsOnLocalStorage();
}
- handleDisconnectWallet(event?: Event): void {
- event?.preventDefault();
-
- this.peraWallet.disconnect().catch(error => {
- console.error('Error disconnecting wallet:', error);
- });
+ onLogout() {
+ this.authService.logout();
+ this.logoutRequested.emit();
}
onVolumeChange(volume: number) {
// Normalize volume to 0-1 range cause of the audio player html component
volume /= 100;
-
+
this.soundEffectService.setVolume(volume);
+
+ if (this.authService.user()) {
+ this.updateSettingsOnLocalStorage();
+ }
+ }
+
+ updateSettingsOnLocalStorage() {
+ UtilsService.saveObjToLocalStorage('settingsWindowData', {
+ walletAddress: this.userAccountAddress(),
+ userImageB64: this.userImageB64(),
+ volume: this.soundEffectService.volume(),
+ });
}
-}
\ No newline at end of file
+}
diff --git a/src/app/components/windows/style-guide-window/style-guide-window.component.ts b/src/app/components/windows/style-guide-window/style-guide-window.component.ts
index 987027a..6a536b4 100644
--- a/src/app/components/windows/style-guide-window/style-guide-window.component.ts
+++ b/src/app/components/windows/style-guide-window/style-guide-window.component.ts
@@ -1,13 +1,13 @@
import { CommonModule } from '@angular/common';
-import { Component, input, signal, computed, inject } from '@angular/core';
+import { Component, computed, inject, input, signal } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { environment } from '../../../../environments/environment.local';
import { DraggableDirective } from '../../../directives/draggable.directive';
+import { UserService } from '../../../services/general/user.service';
import { CvdCheckboxComponent } from "../../shared/corvid-checkbox/corvid-checkbox.component";
import { PixelIconComponent } from '../../shared/pixel-icon/pixel-icon.component';
-import { FloatWindow } from '../float-window/float-window.component';
-import { FormsModule } from '@angular/forms';
-import { UserService } from '../../../services/general/user.service';
-import { environment } from '../../../../environments/environment.local';
import { CvdSliderComponent } from "../../slider/slider.component";
+import { FloatWindow } from '../float-window/float-window.component';
@Component({
selector: 'app-style-guide-window',
@@ -140,7 +140,6 @@ export class StyleGuideWindowComponent extends FloatWindow {
}
});
- // TODO: Setup endpoint call to actually test the Backend calls
this.userService.saveUserSettings(environment.development_wallet).subscribe({
next: response => {
console.log(response);
diff --git a/src/app/components/windows/tetris-window/tetris-window.component.ts b/src/app/components/windows/tetris-window/tetris-window.component.ts
index 58ab54e..99bc00f 100644
--- a/src/app/components/windows/tetris-window/tetris-window.component.ts
+++ b/src/app/components/windows/tetris-window/tetris-window.component.ts
@@ -1,11 +1,11 @@
-import { Component, OnInit, OnDestroy, signal, HostListener, input, ElementRef, viewChild } from '@angular/core';
-import { FloatWindow } from '../float-window/float-window.component';
-import { DraggableDirective } from '../../../directives/draggable.directive';
import { CommonModule } from '@angular/common';
-import { MatIconModule } from '@angular/material/icon';
+import { Component, ElementRef, HostListener, input, OnDestroy, OnInit, signal, viewChild } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { DraggableDirective } from '../../../directives/draggable.directive';
import { ResizableDirective } from '../../../directives/resizable.directive';
import { PixelIconComponent } from "../../shared/pixel-icon/pixel-icon.component";
+import { FloatWindow } from '../float-window/float-window.component';
interface Position {
x: number;
diff --git a/src/app/directives/draggable.directive.ts b/src/app/directives/draggable.directive.ts
index b8d4f06..c4eec6e 100644
--- a/src/app/directives/draggable.directive.ts
+++ b/src/app/directives/draggable.directive.ts
@@ -1,4 +1,4 @@
-import { Directive, ElementRef, inject, Input, OnInit, OnDestroy } from '@angular/core';
+import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ZIndexManagerService } from '../services/general/z-index-manager.service';
@Directive({
diff --git a/src/app/directives/resizable.directive.ts b/src/app/directives/resizable.directive.ts
index 1169b5b..c7e6bc9 100644
--- a/src/app/directives/resizable.directive.ts
+++ b/src/app/directives/resizable.directive.ts
@@ -1,4 +1,4 @@
-import { Directive, ElementRef, input, OnChanges, OnDestroy, OnInit, output, SimpleChanges } from '@angular/core';
+import { Directive, ElementRef, input, OnDestroy, OnInit, output } from '@angular/core';
export interface ResizeEvent {
width: number;
@@ -65,7 +65,7 @@ export class ResizableDirective implements OnInit, OnDestroy {
private createResizeHandles(): void {
const element = this.el.nativeElement;
- // TODO: Inconsistent approach - handles should be created as child elements but need higher z-index than content
+ // Inconsistent approach - handles should be created as child elements but need higher z-index than content
// Consider using a wrapper div or CSS-only approach with ::after/::before pseudo-elements
// Create handles for all 8 resize positions
diff --git a/src/app/features/auth/callback/callback.component.ts b/src/app/features/auth/callback/callback.component.ts
new file mode 100644
index 0000000..7e95182
--- /dev/null
+++ b/src/app/features/auth/callback/callback.component.ts
@@ -0,0 +1,23 @@
+import { Component, inject, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { AuthService } from '../../../services/general/auth.service';
+
+@Component({
+ template: `Completing verification...
`
+})
+export class CallbackComponent implements OnInit {
+ private route: ActivatedRoute = inject(ActivatedRoute);
+ private auth: AuthService = inject(AuthService);
+ private router: Router = inject(Router);
+
+ ngOnInit() {
+ const jwt = this.route.snapshot.queryParamMap.get('jwt');
+ const refreshToken = this.route.snapshot.queryParamMap.get('refresh_token');
+
+ if (jwt && refreshToken) {
+ this.auth.handleCallback(jwt, refreshToken);
+ } else {
+ this.router.navigate(['/']);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/interceptors/auth.interceptor.ts b/src/app/interceptors/auth.interceptor.ts
new file mode 100644
index 0000000..2be42d0
--- /dev/null
+++ b/src/app/interceptors/auth.interceptor.ts
@@ -0,0 +1,34 @@
+import { HttpErrorResponse, HttpInterceptorFn } from "@angular/common/http";
+import { inject } from "@angular/core";
+import { catchError, from, switchMap, throwError } from "rxjs";
+import { AuthService } from "../services/general/auth.service";
+
+/** Attaches the Bearer token to every request and transparently refreshes it on 401 */
+export const authInterceptor: HttpInterceptorFn = (req, next) => {
+ const auth = inject(AuthService);
+ const token = auth.getAccessToken();
+ const REFRESH_KEY = auth.getRefreshKey();
+
+ const authedReq = token ? req.clone({ setHeaders: {Authorization: `Bearer ${token}`} }) : req;
+
+ return next(authedReq).pipe(
+ catchError((error: HttpErrorResponse) => {
+ if (error.status === 401) {
+ const refreshToken = localStorage.getItem(REFRESH_KEY);
+ if (refreshToken) {
+ return from(auth.refresh(refreshToken)).pipe(
+ switchMap(() => {
+ const newToken = auth.getAccessToken();
+ return next(req.clone({ setHeaders: { Authorization: `Bearer ${newToken}` } }))
+ }),
+ catchError(() => {
+ auth.logout();
+ return throwError(() => error);
+ })
+ );
+ }
+ }
+ return throwError(() => error);
+ })
+ );
+}
\ No newline at end of file
diff --git a/src/app/interfaces/corvid-nft-settings.ts b/src/app/interfaces/corvid-nft-settings.ts
deleted file mode 100644
index b52e4c3..0000000
--- a/src/app/interfaces/corvid-nft-settings.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export interface CorvidNftSettings {
- currentAccount: string;
- source: "website" | "mono" | "corvidBot";
-
- currentTheme: string;
-}
-
-// meta_extra_data: {
-
-// }
\ No newline at end of file
diff --git a/src/app/interfaces/corvid-nft.interface.ts b/src/app/interfaces/corvid-nft.interface.ts
index b131d6a..6273061 100644
--- a/src/app/interfaces/corvid-nft.interface.ts
+++ b/src/app/interfaces/corvid-nft.interface.ts
@@ -1,7 +1,9 @@
export interface CorvidNft {
name: string;
+ assetId?: number;
standard: string;
image: string;
+ imageB64?: string;
imageIpfsUrl?: string;
image_mime_type: string;
description: string;
diff --git a/src/app/interfaces/corvid-user.interface.ts b/src/app/interfaces/corvid-user.interface.ts
new file mode 100644
index 0000000..141bf3a
--- /dev/null
+++ b/src/app/interfaces/corvid-user.interface.ts
@@ -0,0 +1,7 @@
+export interface CorvidUser {
+ walletAddress: string; // JWT sub
+ tier: string;
+ balance: number;
+ platform: string;
+ expiresAt: Date;
+}
\ No newline at end of file
diff --git a/src/app/interfaces/pera-wallet-connect-options.ts b/src/app/interfaces/pera-wallet-connect-options.ts
deleted file mode 100644
index 97bffdb..0000000
--- a/src/app/interfaces/pera-wallet-connect-options.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * ChainId determines which Algorand network your dApp uses.
- * - MainNet: 416001
- * - TestNet: 416002
- * - BetaNet: 416003
- * - All Networks: 4160
- */
-export enum AlgorandChainIDs {
- MainNet = 416001,
- TestNet = 416002,
- BetaNet = 416003,
- All = 4160
-}
-
-export interface PeraWalletConnectOptions {
- shouldShowSignTxnToast?: boolean;
- chainId?: AlgorandChainIDs;
-}
\ No newline at end of file
diff --git a/src/app/services/asset.service.ts b/src/app/services/asset.service.ts
index bc755da..06de1d0 100644
--- a/src/app/services/asset.service.ts
+++ b/src/app/services/asset.service.ts
@@ -1,12 +1,8 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
-import algosdk from "algosdk";
-import { CID } from 'multiformats/cid';
-import * as Digest from "multiformats/hashes/digest";
-import { forkJoin, map, Observable, of } from 'rxjs';
+import { Observable } from 'rxjs';
import { environment } from '../../environments/environment.local';
import { CorvidNft, SiteSettingsMetadata } from '../interfaces/corvid-nft.interface';
-import { AssetHoldings, CreatedAssetsResponse, FetchNFTsResponse } from '../interfaces/asset.interfaces';
import { SoundEffectService } from './general/sound-effect.service';
import { ThemeService } from './general/theme.service';
@@ -16,21 +12,21 @@ export enum MembershipStatus {
UNLICENSED
}
+export interface FluentPage {
+ items: CorvidNft[];
+ metadata: {
+ page: number;
+ per: number;
+ total: number;
+ };
+}
+
@Injectable({
providedIn: 'root',
})
export class AssetService {
-
- // TODO: Change name of this service to asset service
- private corvid_asa_token_id: string = "3225439167";
- private indexer_url = 'https://mainnet-idx.4160.nodely.dev';
- private ipfsGateway = 'https://ipfs.algonode.xyz/ipfs/';
- private algod_url: string = "https://mainnet-api.4160.nodely.dev";
private corvid_wallet = environment.corvid_wallet;
-
- private ipfsFallbacks = ["https://ipfs.io/ipfs/", "https://ipfs.filebase.io/ipfs/", "https://dweb.link/ipfs/"];
- private currentIpfsFallbackIndex = 0;
- private activeGateway = this.ipfsGateway;
+ private serverUrl = environment.serverUrl;
private httpClient: HttpClient = inject(HttpClient);
private themeService: ThemeService = inject(ThemeService);
@@ -38,132 +34,38 @@ export class AssetService {
constructor() {}
- listCreatedAssets(pageSize: number, nextToken: string | null, fallback: boolean = false): Observable {
- let params: HttpParams = new HttpParams();
- params = params.append('limit', pageSize);
-
- if (fallback) {
- this.activeGateway = this.ipfsFallbacks[this.currentIpfsFallbackIndex];
- this.currentIpfsFallbackIndex = (this.currentIpfsFallbackIndex + 1) % this.ipfsFallbacks.length;
- if (!environment.production) {
- console.log("Switched to fallback gateway: " + this.activeGateway);
- }
- }
-
- const gateway = this.activeGateway;
-
- if (nextToken) {
- params = params.append('next', nextToken);
- }
-
- return this.httpClient.get(`${this.indexer_url}/v2/accounts/${this.corvid_wallet}/created-assets`, { params }).pipe(
- map(rawResponse => {
- const createdAssetsResponse: CreatedAssetsResponse = {
- currentRound: rawResponse['current-round'],
- nextToken: rawResponse['next-token'] ?? null,
- assets: rawResponse.assets.map((asset: any) => {
- let cid = this.extractCidFromReserveAddress(asset.params.reserve);
- const metadataIpfs = `${gateway}${cid}`;
-
- return {
- createdAtRound: asset['created-at-round'],
- index: asset.index,
- params: {
- clawback: asset.params.clawback,
- creator: asset.params.creator,
- decimals: asset.params.decimals,
- defaultFrozen: asset.params['default-frozen'],
- freeze: asset.params.freeze,
- manager: asset.params.manager,
- name: asset.params.name,
- nameb64: asset.params['name-b64'],
- reserve: asset.params.reserve,
- total: asset.params.total,
- unitName: asset.params['unit-name'],
- unitNameb64: asset.params['unit-name-b64'],
- url: asset.params.url,
- urlb64: asset.params['url-b64'],
- metadataIpfs: metadataIpfs
- }
- };
- })
- };
-
- return createdAssetsResponse;
- })
- );
+ // Remember to add API key to use the corvid backend
+ // Add "X-API-Key: API_KEY_HERE" header to the request
+ // const headers = { 'X-API-Key': 'API_KEY_HERE' };
+ // return this.httpClient.get(`${this.serverUrl}/nfts`, {headers});
+
+ /**
+ * Fetch Corvid NFTs from the corvid verify backend
+ * @returns Observable containing the fetched NFTs
+ */
+ getCorvidNFTsFromVerify(): Observable {
+ return this.httpClient.get(`${this.serverUrl}/nfts`);
}
- listCorvidNftsFromCreatedAssets(createdAssetsResponse: CreatedAssetsResponse): Observable {
- if (!createdAssetsResponse || createdAssetsResponse.assets.length === 0) {
- console.log('No assets found.');
- return of([]); // Return an observable of an empty array
- }
-
- const metadataUrls = createdAssetsResponse.assets.map(asset => asset.params.metadataIpfs);
- const metadataRequests: Observable[] = metadataUrls.map(url =>
- this.httpClient.get(url)
- );
-
- return forkJoin(metadataRequests).pipe(
- map(nfts => {
- nfts.forEach(nft => {
- nft.imageIpfsUrl = nft.image.replace('ipfs://', this.ipfsGateway);
- });
-
- return nfts;
- })
- );
+ /**
+ * Fetch a paginated list of Corvid NFTs from the corvid verify backend
+ * @param requestPage The page number to request
+ * @param pageSize The number of items per page
+ * @returns Observable containing the paginated NFTs
+ */
+ getCorvidNFTsPaginated(requestPage: number, pageSize: number): Observable {
+ const params = new HttpParams()
+ .append('page', requestPage.toString())
+ .append('per', pageSize.toString());
+
+ return this.httpClient.get(`${this.serverUrl}/nfts/paginated`, { params });
}
- private extractCidFromReserveAddress(reserveAddress: string): string {
- const decodedReserve = algosdk.decodeAddress(reserveAddress)?.publicKey;
- const multihash = Digest.create(0x12, decodedReserve);
- const cid = CID.create(1, 0x55, multihash); // 0x55 is the code for raw binary
- return cid.toString() ?? '';
- }
-
- private checkHolderConfig(walletAddress: string, corvidNFTs: AssetHoldings[]): Observable {
- // Get metadata from reserve addresses of all CorvidNfts of the user
- const reserveAddresses = corvidNFTs.map(nft => nft['asset-params'].reserve);
-
- return this.getMetadataFromReserveAddresses(reserveAddresses).pipe(
- map(nfts => {
- // Check if any NFT has the user's wallet address in its siteSettingsMetadata
- const nftWithConfig = nfts.find(nft => nft.extra?.siteSettingsMetadata?.walletAddress === walletAddress);
-
- // If found, load the holder settings
- if (nftWithConfig) {
- this.loadHolderSettings(nftWithConfig.extra.siteSettingsMetadata as SiteSettingsMetadata);
- }
-
- // Return Holder status since user has Corvid NFTs
- return MembershipStatus.NFT_HOLDER;
- })
- );
- }
-
- private getMetadataFromReserveAddresses(reserveAddresses: string[]): Observable {
- const metadataRequests: Observable[] = reserveAddresses.map(address =>
- this.httpClient.get(`${this.ipfsGateway}${this.extractCidFromReserveAddress(address)}`)
- );
-
- return forkJoin(metadataRequests).pipe(
- map(nfts => {
- nfts.forEach(nft => {
- nft.imageIpfsUrl = nft.image.replace('ipfs://', this.ipfsGateway);
- });
-
- return nfts;
- })
- );
- }
+ getWalletAssetsHolding(address: string): Observable {
+ const params = new HttpParams()
+ .append('wallet', address);
- // TODO: Make a database check of people with wallet but without any Corvid NFT
- // Check for purchased premium licenses etc
- private checkNonHolderLicense(walletAddress: string): Observable {
- // Implement your license checking logic here
- return of(MembershipStatus.UNLICENSED);
+ return this.httpClient.get(`${this.serverUrl}/nfts/holdings`, { params });
}
// Load holder settings into the application state
diff --git a/src/app/services/general/auth.service.ts b/src/app/services/general/auth.service.ts
new file mode 100644
index 0000000..2453e1c
--- /dev/null
+++ b/src/app/services/general/auth.service.ts
@@ -0,0 +1,109 @@
+import { HttpClient } from "@angular/common/http";
+import { inject, Injectable, signal } from "@angular/core";
+import { Router } from "@angular/router";
+import { firstValueFrom } from "rxjs";
+import { environment } from "../../../environments/environment.local";
+import { CorvidUser } from "../../interfaces/corvid-user.interface";
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthService {
+
+ private accessToken: string | null = null;
+ private readonly REFRESH_KEY = 'corvid_refresh_token';
+ private readonly VERIFY_KEY = environment.serverUrl;
+
+ private readonly _user = signal(null);
+ readonly user = this._user.asReadonly();
+
+ private http: HttpClient = inject(HttpClient);
+ private router: Router = inject(Router);
+
+ constructor() {}
+
+ /** Called on app init - restore session from refresh token if present */
+ async init(): Promise {
+ const refreshToken = localStorage.getItem(this.REFRESH_KEY);
+ if (refreshToken) {
+ try { await this.refresh(refreshToken); } catch { this.clearSession(); }
+ }
+ }
+
+ /** Starts the auth process to get the tokens */
+ async startVerification(): Promise {
+ const res = await firstValueFrom(
+ this.http.post<{ url: string }>(`${this.VERIFY_KEY}/api/v1/auth/start`, {
+ platform: 'web',
+ redirect_uri: `${environment.appUrl}/auth/callback`,
+ })
+ );
+
+ window.location.href = res.url; // Navigate to verify-app
+ }
+
+ /** Exchange a refresh token for a new access JWT. Rotates the refresh token. */
+ async refresh(refreshToken: string): Promise {
+ const res = await firstValueFrom(
+ this.http.post(`${this.VERIFY_KEY}/api/v1/auth/refresh`, {
+ refresh_token: refreshToken,
+ })
+ );
+
+ this.setSession(res.jwt, res.refresh_token);
+ }
+
+ /** Called from the callback component after redirect */
+ handleCallback(jwt: string, refreshToken: string): void {
+ this.setSession(jwt, refreshToken);
+ this.router.navigate(['/']);
+ }
+
+ getAccessToken(): string | null {
+ return this.accessToken;
+ }
+
+ getRefreshKey(): string {
+ return this.REFRESH_KEY;
+ }
+
+ async logout(): Promise {
+ const refreshToken = localStorage.getItem(this.REFRESH_KEY);
+ if (refreshToken) {
+ // Best-effort revoke - don't block logout if fails
+ this.http.post(`${this.VERIFY_KEY}/api/v1/auth/revoke`, { refresh_token: refreshToken }).subscribe({});
+ }
+
+ this.clearSession();
+ this.router.navigate(['/']);
+ }
+
+ private setSession(jwt: string, refreshToken: string): void {
+ this.accessToken = jwt;
+ localStorage.setItem(this.REFRESH_KEY, refreshToken);
+
+ const payload = this.decodeJwt(jwt);
+ this._user.set({
+ walletAddress: payload.sub,
+ tier: payload.tier,
+ balance: payload.balance,
+ platform: payload.platform,
+ expiresAt: new Date(payload.exp * 1000),
+ });
+ }
+
+ private clearSession(): void {
+ this.accessToken = null;
+ localStorage.removeItem(this.REFRESH_KEY);
+ this._user.set(null);
+ }
+
+ private decodeJwt(jwt: string): any {
+ return JSON.parse(atob(jwt.split('.')[1]));
+ }
+}
+
+interface TokenResponse {
+ jwt: string;
+ refresh_token: string;
+}
\ No newline at end of file
diff --git a/src/app/services/general/sound-effect.service.ts b/src/app/services/general/sound-effect.service.ts
index 03824d9..005174e 100644
--- a/src/app/services/general/sound-effect.service.ts
+++ b/src/app/services/general/sound-effect.service.ts
@@ -13,9 +13,13 @@ export class SoundEffectService {
// Private state
private soundFiles = [
- '/sfx/Mouse_Hit.mp3',
'/sfx/Click_Interaction.mp3'
];
+ // private soundFiles = [
+ // '/sfx/Mouse_Hit.mp3',
+ // '/sfx/Click_Interaction.mp3'
+ // ];
+
private audioPool: HTMLAudioElement[] = [];
private currentPoolIndex = 0;
private readonly POOL_SIZE = 3;
diff --git a/src/app/services/general/user.service.ts b/src/app/services/general/user.service.ts
index c299055..df4d259 100644
--- a/src/app/services/general/user.service.ts
+++ b/src/app/services/general/user.service.ts
@@ -1,24 +1,16 @@
import { HttpClient } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
+import { Observable } from "rxjs";
import { environment } from "../../../environments/environment.local";
import { SiteSettingsMetadata } from "../../interfaces/corvid-nft.interface";
import { SoundEffectService } from "./sound-effect.service";
import { ThemeService } from "./theme.service";
-import { Observable } from "rxjs";
@Injectable({
providedIn: 'root',
})
export class UserService {
-
- // TODO: Figure out the ipfs gateway and how to extract the CID from the ipft url from the api list response
- // https://nodely.io/swagger/index.html?url=/swagger/api/4160/indexer.oas3.yml#/common/makeHealthCheck ????????
- // IPFS GATEWAY https://nodely.io/ipfs-gateway ?????
- private corvid_asa_token_id: string = "3225439167";
- private indexer_url = 'https://mainnet-idx.4160.nodely.dev';
- private ipfsGateway = 'https://ipfs.algonode.xyz/ipfs/';
private algod_url: string = "https://mainnet-api.4160.nodely.dev";
- private corvid_wallet = environment.corvid_wallet;
private serverUrl: string = environment.serverUrl;
diff --git a/src/app/services/general/utils.service.ts b/src/app/services/general/utils.service.ts
index 206a335..4bf01bb 100644
--- a/src/app/services/general/utils.service.ts
+++ b/src/app/services/general/utils.service.ts
@@ -1,6 +1,6 @@
import { HttpParams } from "@angular/common/http";
import { Injectable } from "@angular/core";
-import {MatDialog} from '@angular/material/dialog';
+import { MatDialog } from '@angular/material/dialog';
import { Observable, of } from "rxjs";
@Injectable({
diff --git a/src/app/services/soundcloud.service.ts b/src/app/services/soundcloud.service.ts
index 18774ae..c9e7169 100644
--- a/src/app/services/soundcloud.service.ts
+++ b/src/app/services/soundcloud.service.ts
@@ -1,7 +1,7 @@
-import { HttpClient, HttpParams } from "@angular/common/http";
+import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
-import { UtilsService } from "./general/utils.service";
import { Observable } from "rxjs";
+import { UtilsService } from "./general/utils.service";
// Use camelCase and remap it?
export interface SoundCloudRequestParams {
diff --git a/src/index.html b/src/index.html
index ea25d32..26f53cb 100644
--- a/src/index.html
+++ b/src/index.html
@@ -13,7 +13,6 @@
-
diff --git a/src/main.ts b/src/main.ts
index 5df75f9..40016cf 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,6 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
-import { appConfig } from './app/app.config';
import { App } from './app/app';
+import { appConfig } from './app/app.config';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));
diff --git a/src/polyfills.ts b/src/polyfills.ts
index dc274a1..47b51dd 100644
--- a/src/polyfills.ts
+++ b/src/polyfills.ts
@@ -1,6 +1,6 @@
/**
* Polyfills for browser compatibility with Node.js modules
- * Required for @perawallet/connect and algosdk
+ * Required for multiformats and other packages that expect a Node.js-like environment
*/
import { Buffer } from 'buffer';