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 @@ -
-
- - - -
- @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() }}

{{ 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 @@ - - 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') { +
+
+

Choose Profile Picture

+ +
+
+ @for (asset of userAssets(); track asset.name) { +
+ +
+ } +
+
+ } @else { +
+ + }
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';