From e818c808aed2edbdee58bf8c193ca6615605fa07 Mon Sep 17 00:00:00 2001 From: Melisa Anabella Rossi Date: Tue, 16 Jan 2024 18:56:19 -0300 Subject: [PATCH] feat: add ethereum address mapping (#3015) --- package-lock.json | 600 +++++++++++++++--- package.json | 1 + .../ENSDetailPage/ENSDetailPage.container.ts | 31 + .../ENSDetailPage/ENSDetailPage.module.css | 112 ++++ .../ENSDetailPage/ENSDetailPage.spec.tsx | 154 +++++ .../ENSDetailPage/ENSDetailPage.tsx | 189 ++++++ .../ENSDetailPage/ENSDetailPage.types.ts | 18 + src/components/ENSDetailPage/index.ts | 2 + .../ENSListPage/ENSListPage.container.ts | 5 +- src/components/ENSListPage/ENSListPage.css | 125 ++++ src/components/ENSListPage/ENSListPage.tsx | 216 ++++++- .../ENSListPage/ENSListPage.types.ts | 5 +- src/components/ENSListPage/utils.spec.ts | 21 + src/components/ENSListPage/utils.ts | 6 + .../ENSMapAddressModal.container.ts | 21 + .../ENSMapAddressModal.module.css | 49 ++ .../ENSMapAddressModal.spec.tsx | 61 ++ .../ENSMapAddressModal/ENSMapAddressModal.tsx | 63 ++ .../ENSMapAddressModal.types.ts | 14 + .../Modals/ENSMapAddressModal/index.ts | 2 + src/components/Modals/index.ts | 1 + src/icons/ethereum.svg | 4 + src/modules/ens/actions.ts | 20 +- src/modules/ens/reducer.spec.ts | 104 ++- src/modules/ens/reducer.ts | 33 +- src/modules/ens/sagas.spec.ts | 77 ++- src/modules/ens/sagas.ts | 124 ++-- src/modules/ens/selectors.spec.ts | 77 ++- src/modules/ens/selectors.ts | 7 +- src/modules/ens/types.ts | 5 +- src/modules/features/selectors.spec.ts | 6 +- src/modules/features/selectors.ts | 8 + src/modules/features/types.ts | 3 +- src/modules/location/selectors.spec.ts | 18 +- src/modules/location/selectors.ts | 12 + src/modules/translation/languages/en.json | 40 +- src/modules/translation/languages/es.json | 40 +- src/modules/translation/languages/zh.json | 40 +- src/routing/Routes.tsx | 2 + src/routing/locations.ts | 1 + src/specs/utils.tsx | 47 ++ src/themes/components/CopyToClipboard.css | 17 + 42 files changed, 2225 insertions(+), 156 deletions(-) create mode 100644 src/components/ENSDetailPage/ENSDetailPage.container.ts create mode 100644 src/components/ENSDetailPage/ENSDetailPage.module.css create mode 100644 src/components/ENSDetailPage/ENSDetailPage.spec.tsx create mode 100644 src/components/ENSDetailPage/ENSDetailPage.tsx create mode 100644 src/components/ENSDetailPage/ENSDetailPage.types.ts create mode 100644 src/components/ENSDetailPage/index.ts create mode 100644 src/components/ENSListPage/utils.spec.ts create mode 100644 src/components/ENSListPage/utils.ts create mode 100644 src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.container.ts create mode 100644 src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.module.css create mode 100644 src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.spec.tsx create mode 100644 src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.tsx create mode 100644 src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.types.ts create mode 100644 src/components/Modals/ENSMapAddressModal/index.ts create mode 100644 src/icons/ethereum.svg create mode 100644 src/specs/utils.tsx diff --git a/package-lock.json b/package-lock.json index 92ed414df..305cde010 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,6 +101,7 @@ "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.36.2", "@typescript-eslint/parser": "^5.36.2", + "canvas": "^2.11.2", "concurrently": "^7.2.2", "decentraland-rpc": "^3.1.8", "eslint": "^7.28.0", @@ -4657,6 +4658,119 @@ "resolved": "https://registry.npmjs.org/@magic-sdk/types/-/types-17.3.0.tgz", "integrity": "sha512-0mTFr1qDJ94pOJkFu1oZ/s2KnV7lHgILvWuFh7fs7ugyn7z9M7euP9g+Bv+kEdZ6ja4QlNi+UR0OryaXowv75w==" }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "devOptional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "devOptional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "devOptional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "devOptional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "devOptional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@metamask/safe-event-emitter": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz", @@ -7716,6 +7830,12 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "devOptional": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -9855,17 +9975,6 @@ "node": ">=10" } }, - "node_modules/cacache/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/cacache/node_modules/minipass": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", @@ -9877,18 +9986,6 @@ "node": ">=8" } }, - "node_modules/cacache/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/cacache/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -9900,22 +9997,6 @@ "node": ">=10" } }, - "node_modules/cacache/node_modules/tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -10031,6 +10112,56 @@ } ] }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/canvas/node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "devOptional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/canvas/node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/canvas/node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "devOptional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -10533,6 +10664,15 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "devOptional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/color/node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -10972,7 +11112,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "optional": true + "devOptional": true }, "node_modules/constants-browserify": { "version": "1.0.0", @@ -13027,7 +13167,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "optional": true + "devOptional": true }, "node_modules/depd": { "version": "2.0.0", @@ -16063,6 +16203,28 @@ "node": ">=10" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -16658,7 +16820,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "optional": true + "devOptional": true }, "node_modules/has-value": { "version": "1.0.0", @@ -21067,6 +21229,29 @@ "node": ">=8" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -21428,10 +21613,10 @@ } }, "node_modules/nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "optional": true + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "devOptional": true }, "node_modules/nanoid": { "version": "3.3.4", @@ -21779,6 +21964,21 @@ "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==", "optional": true }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "devOptional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -29024,6 +29224,22 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", @@ -29098,6 +29314,33 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tdigest": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", @@ -32510,7 +32753,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "optional": true, + "devOptional": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -36194,6 +36437,96 @@ "resolved": "https://registry.npmjs.org/@magic-sdk/types/-/types-17.3.0.tgz", "integrity": "sha512-0mTFr1qDJ94pOJkFu1oZ/s2KnV7lHgILvWuFh7fs7ugyn7z9M7euP9g+Bv+kEdZ6ja4QlNi+UR0OryaXowv75w==" }, + "@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "devOptional": true, + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "dependencies": { + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "devOptional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "devOptional": true + }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "devOptional": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "devOptional": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true + } + } + }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "devOptional": true, + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "devOptional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "@metamask/safe-event-emitter": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz", @@ -38713,6 +39046,12 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "devOptional": true + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -40434,14 +40773,6 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, "minipass": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", @@ -40450,32 +40781,10 @@ "yallist": "^4.0.0" } }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } } } }, @@ -40562,6 +40871,45 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001353.tgz", "integrity": "sha512-GqItFu1lCW4OGd4f47TVQXAGxca8K9Bz3cBb872ZskMo6FIQhiHCc7QjBL7Bb4XannbV+Gq0yHhFVxONW6C/XQ==" }, + "canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "devOptional": true, + "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "dependencies": { + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "devOptional": true, + "requires": { + "mimic-response": "^2.0.0" + } + }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "devOptional": true + }, + "simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "devOptional": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + } + } + }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -40967,6 +41315,12 @@ "simple-swizzle": "^0.2.2" } }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "devOptional": true + }, "colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -41314,7 +41668,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "optional": true + "devOptional": true }, "constants-browserify": { "version": "1.0.0", @@ -42884,7 +43238,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "optional": true + "devOptional": true }, "depd": { "version": "2.0.0", @@ -45271,6 +45625,24 @@ "universalify": "^2.0.0" } }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -45732,7 +46104,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "optional": true + "devOptional": true }, "has-value": { "version": "1.0.0", @@ -49223,6 +49595,25 @@ } } }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -49530,10 +49921,10 @@ "integrity": "sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g==" }, "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "optional": true + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "devOptional": true }, "nanoid": { "version": "3.3.4", @@ -49850,6 +50241,15 @@ "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==", "optional": true }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "devOptional": true, + "requires": { + "abbrev": "1" + } + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -55627,6 +56027,36 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" }, + "tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, "tar-fs": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", @@ -58463,7 +58893,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "optional": true, + "devOptional": true, "requires": { "string-width": "^1.0.2 || 2 || 3 || 4" } diff --git a/package.json b/package.json index 1701b210e..ca7cbff3f 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^5.36.2", "@typescript-eslint/parser": "^5.36.2", + "canvas": "^2.11.2", "concurrently": "^7.2.2", "decentraland-rpc": "^3.1.8", "eslint": "^7.28.0", diff --git a/src/components/ENSDetailPage/ENSDetailPage.container.ts b/src/components/ENSDetailPage/ENSDetailPage.container.ts new file mode 100644 index 000000000..ee9d77ff4 --- /dev/null +++ b/src/components/ENSDetailPage/ENSDetailPage.container.ts @@ -0,0 +1,31 @@ +import { connect } from 'react-redux' +import { Dispatch } from 'redux' +import { push } from 'connected-react-router' +import { getENSBySubdomain, getLoading } from 'modules/ens/selectors' +import { RootState } from 'modules/common/types' +import { getENSName } from 'modules/location/selectors' +import { openModal } from 'modules/modal/actions' +import { MapStateProps, MapDispatchProps } from './ENSDetailPage.types' +import ENSDetailPage from './ENSDetailPage' +import { isLoadingType } from 'decentraland-dapps/dist/modules/loading/selectors' +import { FETCH_ENS_REQUEST, fetchENSRequest } from 'modules/ens/actions' +import { getAvatar, getName } from 'modules/profile/selectors' + +const mapState = (state: RootState): MapStateProps => { + const name = getENSName(state) + return { + name, + ens: name ? getENSBySubdomain(state, `${name}.dcl.eth`) : null, + isLoading: isLoadingType(getLoading(state), FETCH_ENS_REQUEST), + alias: getName(state), + avatar: getAvatar(state) + } +} + +const mapDispatch = (dispatch: Dispatch): MapDispatchProps => ({ + onFetchENS: (name: string) => dispatch(fetchENSRequest(name)), + onOpenModal: (name, metadata) => dispatch(openModal(name, metadata)), + onNavigate: path => dispatch(push(path)) +}) + +export default connect(mapState, mapDispatch)(ENSDetailPage) diff --git a/src/components/ENSDetailPage/ENSDetailPage.module.css b/src/components/ENSDetailPage/ENSDetailPage.module.css new file mode 100644 index 000000000..b34dccebc --- /dev/null +++ b/src/components/ENSDetailPage/ENSDetailPage.module.css @@ -0,0 +1,112 @@ +.main { + display: flex; + padding: 35px; + background: var(--secondary); + border-radius: 10px; + gap: 50px; +} + +.fields { + display: flex; + flex-direction: column; + flex: 1; + gap: 20px; +} + +.ensImage { + width: 326px; + height: 326px; + border-radius: 10px; +} + +.field { + display: flex; + flex-direction: column; + flex: 1; + gap: 24px; +} + +.fieldContainer { + display: flex; +} + +.fieldContainer:first-child { + border-bottom: 1px solid var(--secondary-text); + padding: 40px 0 20px 0; +} + +.fieldTitle { + font-size: 14px; + color: var(--secondary-text); +} + +.subdomain { + display: flex; + font-weight: bold; + font-size: 40px; + line-height: 40px; + color: var(--secondary-text); + align-items: center; + gap: 10px; +} + +.subdomain span span { + color: white; +} + +.unassign { + border-radius: 8px; + background: #302c36; + color: white; + font-size: 16px; + padding: 0 20px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + width: fit-content; +} + +.actionBtn { + width: fit-content; + min-width: none; +} + +.avatar { + display: flex; + align-items: center; + gap: 8px; + color: var(--secondary-text); +} + +.avatarImg { + width: 30px; + height: 30px; + border-radius: 50%; + background: #43404a; +} + +.avatarName { + color: white; + font-weight: bold; +} + +.ensBtn:global(.ui.button) { + background: #302c36; + color: white; + width: fit-content; + min-width: none; +} + +.address { + display: flex; + align-items: center; + gap: 10px; +} + +.returnLink:global(.ui.button) { + color: var(--secondary-text) !important; + font-size: 20px; + margin-bottom: 30px; + text-transform: none; +} diff --git a/src/components/ENSDetailPage/ENSDetailPage.spec.tsx b/src/components/ENSDetailPage/ENSDetailPage.spec.tsx new file mode 100644 index 000000000..033685ad5 --- /dev/null +++ b/src/components/ENSDetailPage/ENSDetailPage.spec.tsx @@ -0,0 +1,154 @@ +import { ENS } from 'modules/ens/types' +import { renderWithProviders } from 'specs/utils' +import ENSDetailPage from './ENSDetailPage' +import { Props } from './ENSDetailPage.types' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' +import { getCroppedAddress } from 'components/ENSListPage/utils' + +jest.mock('components/LoggedInDetailPage', () => ({ children }: any) =>
{children}
) +function renderENSDetailPage(props: Partial) { + return renderWithProviders( + + ) +} + +const ensSample: ENS = { + name: 'test', + subdomain: 'test', + content: '', + ensOwnerAddress: '0xtest2', + nftOwnerAddress: '0xtest1', + resolver: '0xtest3', + tokenId: '', + ensAddressRecord: '' +} + +describe('when ens is defined', () => { + let ens: ENS + + describe('and the name is the same as the alias', () => { + let alias: string + beforeEach(() => { + ens = { + ...ensSample, + name: 'test' + } + alias = 'test' + }) + + it('should show alias info', () => { + const screen = renderENSDetailPage({ ens, alias }) + expect(screen.getByTestId('alias-avatar')).toBeInTheDocument() + }) + }) + + describe('and the name is not the same as the alias', () => { + let alias: string + beforeEach(() => { + ens = { + ...ensSample, + name: 'test' + } + alias = 'test2' + }) + + it('should not show alias info', () => { + const screen = renderENSDetailPage({ ens, alias }) + expect(screen.queryByTestId('alias-avatar')).not.toBeInTheDocument() + }) + + it('should show assigned as alias button', () => { + const screen = renderENSDetailPage({ ens, alias }) + expect(screen.getByRole('button', { name: t('ens_detail_page.set_as_primary') })).toBeInTheDocument() + }) + }) + + describe('and there is already an address assigned to the ens', () => { + let address: string + beforeEach(() => { + address = '0xA4f689625F6F51AdF691a64D38772BE85090test' + ens = { + ...ensSample, + ensAddressRecord: address + } + }) + + it('should show cropped address info', () => { + const screen = renderENSDetailPage({ ens }) + expect(screen.getByText(getCroppedAddress(address))).toBeInTheDocument() + }) + + it('should show edit address button', () => { + const screen = renderENSDetailPage({ ens }) + expect(screen.getByRole('button', { name: t('ens_detail_page.edit_address') })).toBeInTheDocument() + }) + }) + + describe('and there is no address assigned to the ens', () => { + beforeEach(() => { + ens = { + ...ensSample, + ensAddressRecord: '' + } + }) + + it('should show add address button', () => { + const screen = renderENSDetailPage({ ens }) + expect(screen.getByRole('button', { name: t('ens_list_page.button.link_to_address') })).toBeInTheDocument() + }) + }) + + describe('and the name has a linked land', () => { + describe('and the land is a coord', () => { + beforeEach(() => { + ens = { + ...ensSample, + landId: '4,4' + } + }) + it('should show land field info', () => { + const screen = renderENSDetailPage({ ens }) + expect(screen.getByTestId('land-field')).toBeInTheDocument() + }) + }) + + describe('and the land is an estate', () => { + describe('and the land is a coord', () => { + beforeEach(() => { + ens = { + ...ensSample, + landId: '4' + } + }) + it('should estate field button', () => { + const screen = renderENSDetailPage({ ens }) + expect(screen.getByTestId('estate-field')).toBeInTheDocument() + }) + }) + }) + }) + + describe('and the ens does not have a linked land', () => { + beforeEach(() => { + ens = { + ...ensSample, + landId: '' + } + }) + + it('should show add address button', () => { + const screen = renderENSDetailPage({ ens }) + expect(screen.getByRole('button', { name: t('ens_list_page.button.assign_to_land') })).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/ENSDetailPage/ENSDetailPage.tsx b/src/components/ENSDetailPage/ENSDetailPage.tsx new file mode 100644 index 000000000..6fd30c02a --- /dev/null +++ b/src/components/ENSDetailPage/ENSDetailPage.tsx @@ -0,0 +1,189 @@ +import { useCallback, useEffect, useMemo } from 'react' +import { config } from 'config' +import { Link } from 'react-router-dom' +import { Button, Icon as DCLIcon } from 'decentraland-ui' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' +import { isCoords } from 'modules/land/utils' +import { locations } from 'routing/locations' +import CopyToClipboard from 'components/CopyToClipboard/CopyToClipboard' +import LoggedInDetailPage from 'components/LoggedInDetailPage' +import { getCroppedAddress } from 'components/ENSListPage/utils' +import { NavigationTab } from 'components/Navigation/Navigation.types' +import Icon from 'components/Icon' +import ethereumImg from '../../icons/ethereum.svg' +import { Props } from './ENSDetailPage.types' +import styles from './ENSDetailPage.module.css' + +export default function ENSDetailPage(props: Props) { + const { ens, isLoading, alias, avatar, name, onNavigate, onOpenModal, onFetchENS } = props + const imgUrl = useMemo( + () => (ens ? `http://marketplace-api.decentraland.zone/v1/ens/generate?ens=${ens.name}&width=330&height=330` : ''), + [ens] + ) + + useEffect(() => { + if (name) { + onFetchENS(name) + } + }, [name, onFetchENS]) + + const handleAssignENSAddress = useCallback(() => { + onOpenModal('EnsMapAddressModal', { ens }) + }, [onOpenModal, ens]) + + const handleSetAsAlias = useCallback(() => { + onOpenModal('UseAsAliasModal', { newName: ens?.name }) + }, [onOpenModal, ens?.name]) + + const handleAssignENS = useCallback(() => { + onNavigate(locations.ensSelectLand(ens?.subdomain)) + }, [onNavigate, ens?.subdomain]) + + const aliasField = useMemo(() => { + if (alias !== ens?.name) { + return ( +
+ {t('ens_detail_page.alias')} + {t('ens_detail_page.unassign')} + +
+ ) + } + return ( +
+ {t('ens_detail_page.alias')} + + {avatar ? ( + {avatar.realName} + ) : ( + + )} + {ens.name} + {t('ens_list_page.table.you')} + +
+ ) + }, [ens?.name, avatar, alias, handleSetAsAlias]) + + const addressField = useMemo(() => { + if (!ens?.ensAddressRecord) { + return ( +
+ {t('ens_detail_page.address')} + +
+ ) + } + return ( +
+ {t('ens_detail_page.address')} + + Ethereum + {getCroppedAddress(ens.ensAddressRecord)} + + + + + +
+ ) + }, [ens?.ensAddressRecord, handleAssignENSAddress]) + + const landField = useMemo(() => { + if (!ens?.landId) { + return ( +
+ {t('ens_detail_page.land')} + +
+ ) + } + if (isCoords(ens.landId)) { + return ( +
+ {t('ens_detail_page.land')} +
+ + + {ens?.landId} + + +
+
+ ) + } else { + return ( +
+ {t('ens_detail_page.land')} +
+ + + {`Estate (${ens.landId})`} + + +
+
+ ) + } + }, [ens?.landId, ens?.subdomain, handleAssignENS]) + + return ( + + + + +
+ {ens?.subdomain} +
+
+
+ {t('ens_detail_page.name')} + + + {ens?.name}.dcl.eth + + + + + +
+
+
+ {aliasField} + {addressField} + {landField} +
+
+
+
+ ) +} diff --git a/src/components/ENSDetailPage/ENSDetailPage.types.ts b/src/components/ENSDetailPage/ENSDetailPage.types.ts new file mode 100644 index 000000000..1844d0d47 --- /dev/null +++ b/src/components/ENSDetailPage/ENSDetailPage.types.ts @@ -0,0 +1,18 @@ +import { Avatar } from '@dcl/schemas' +import { fetchENSRequest } from 'modules/ens/actions' +import { ENS } from 'modules/ens/types' +import { openModal } from 'modules/modal/actions' + +export type Props = { + name: string | null + ens: ENS | null + isLoading: boolean + alias: string | null + avatar: Avatar | null + onOpenModal: typeof openModal + onFetchENS: typeof fetchENSRequest + onNavigate: (path: string) => void +} + +export type MapStateProps = Pick +export type MapDispatchProps = Pick diff --git a/src/components/ENSDetailPage/index.ts b/src/components/ENSDetailPage/index.ts new file mode 100644 index 000000000..0e4d6ddd8 --- /dev/null +++ b/src/components/ENSDetailPage/index.ts @@ -0,0 +1,2 @@ +import ENSDetailPage from './ENSDetailPage.container' +export default ENSDetailPage diff --git a/src/components/ENSListPage/ENSListPage.container.ts b/src/components/ENSListPage/ENSListPage.container.ts index b2c771194..99c03cae9 100644 --- a/src/components/ENSListPage/ENSListPage.container.ts +++ b/src/components/ENSListPage/ENSListPage.container.ts @@ -9,6 +9,7 @@ import { FETCH_ENS_LIST_REQUEST } from 'modules/ens/actions' import { getLands, getLoading as getLandsLoading, getError as getLandsError } from 'modules/land/selectors' import { FETCH_LANDS_REQUEST } from 'modules/land/actions' import { getAvatar, getName } from 'modules/profile/selectors' +import { getIsEnsAddressEnabled } from 'modules/features/selectors' import { openModal } from 'modules/modal/actions' import { MapStateProps, MapDispatchProps, MapDispatch } from './ENSListPage.types' import ENSListPage from './ENSListPage' @@ -24,7 +25,9 @@ const mapState = (state: RootState): MapStateProps => ({ isLoadingType(getLandsLoading(state), FETCH_LANDS_REQUEST) || isLoadingType(getLoading(state), FETCH_ENS_LIST_REQUEST) || isLoggingIn(state), - isLoggedIn: isLoggedIn(state) + isLoggedIn: isLoggedIn(state), + isEnsAddressEnabled: getIsEnsAddressEnabled(state), + avatar: getAvatar(state) }) const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({ diff --git a/src/components/ENSListPage/ENSListPage.css b/src/components/ENSListPage/ENSListPage.css index 3539d2ed4..91c196ed5 100644 --- a/src/components/ENSListPage/ENSListPage.css +++ b/src/components/ENSListPage/ENSListPage.css @@ -90,3 +90,128 @@ .ENSListPage .ui.dropdown { margin-right: 20px; } + +.ENSListPage .ens-list-btn.ui.button { + display: flex; + gap: 5px; + align-items: center; + text-transform: none; +} + +.ENSListPage .ens-list-btn .Icon.pin { + width: 15px; + height: 15px; +} + +.ENSListPage .ens-list-avatar { + display: flex; + align-items: center; + gap: 8px; + color: var(--secondary-text); +} + +.ENSListPage .ens-list-land { + display: flex; + align-items: center; + gap: 5px; +} + +.ENSListPage .ens-list-land-coord { + display: flex; + height: 34px; + align-items: center; + padding: 0 10px; + background: var(--secondary); + border-radius: 6px; +} + +.ENSListPage .ens-list-land-redirect-icon { + filter: grayscale(1) brightness(5); +} + +.ENSListPage .ens-list-land-redirect { + width: fit-content; + min-width: unset; + height: 34px; + padding: 0 6px; + display: flex; + align-items: center; +} + +.ENSListPage .ens-list-address { + display: flex; + gap: 5px; + align-items: center; +} + +.ENSListPage .ens-list-address-icon { + width: 25px; +} + +.ENSListPage .ens-page-content { + display: flex; + flex-direction: column; + gap: 20px; +} + +.ENSListPage .ens-page-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.ENSListPage .ens-list-subdomain { + font-weight: bold; + color: var(--secondary-text); +} + +.ENSListPage .ens-list-subdomain span { + color: white; +} + +.ENSListPage .ens-list-name { + display: flex; + align-items: center; + gap: 8px; +} + +.ENSListPage .ens-page-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.ENSListPage .ens-page-actions .ui.dropdown { + margin-left: 15px; +} + +.ENSListPage .ens-list-edit-btn.ui.button { + min-width: unset; + padding-left: 10px; + padding-right: 10px; + text-transform: none; +} + +.ENSListPage .ens-list-name-icon { + width: 30px; + height: 30px; + border-radius: 5px; + margin-right: 5px; +} + +.ENSListPage .ens-list-avatar-img { + width: 30px; + height: 30px; + border-radius: 50%; + background: #43404a; +} + +.ENSListPage .ens-list-avatar-name { + color: white; + font-weight: bold; +} + +.ENSListPage .ens-list-actions { + display: flex; + gap: 10px; +} diff --git a/src/components/ENSListPage/ENSListPage.tsx b/src/components/ENSListPage/ENSListPage.tsx index 3bc42ab3f..2639840ed 100644 --- a/src/components/ENSListPage/ENSListPage.tsx +++ b/src/components/ENSListPage/ENSListPage.tsx @@ -1,20 +1,36 @@ import React from 'react' import { Link } from 'react-router-dom' import { config } from 'config' -import { Popup, Button, Table, Row, Column, Header, Section, Container, Pagination, Dropdown, Empty } from 'decentraland-ui' +import { + Popup, + Button, + Table, + Row, + Column, + Header, + Section, + Container, + Pagination, + Dropdown, + Empty, + Icon as DCLIcon +} from 'decentraland-ui' import { T, t } from 'decentraland-dapps/dist/modules/translation/utils' import { locations } from 'routing/locations' import { isCoords } from 'modules/land/utils' import { ENS } from 'modules/ens/types' import Icon from 'components/Icon' +import CopyToClipboard from 'components/CopyToClipboard/CopyToClipboard' import { NavigationTab } from 'components/Navigation/Navigation.types' import LoggedInDetailPage from 'components/LoggedInDetailPage' +import ethereumImg from '../../icons/ethereum.svg' +import { getCroppedAddress } from './utils' import { Props, State, SortBy } from './ENSListPage.types' import './ENSListPage.css' const PAGE_SIZE = 12 const MARKETPLACE_WEB_URL = config.get('MARKETPLACE_WEB_URL', '') - +const REGISTRAR_CONTRACT_ADDRESS = config.get('REGISTRAR_CONTRACT_ADDRESS', '') export default class ENSListPage extends React.PureComponent { state: State = { sortBy: SortBy.ASC, @@ -27,6 +43,15 @@ export default class ENSListPage extends React.PureComponent { this.props.onNavigate(locations.ensSelectLand(ens.subdomain)) } + handleUseAsAlias = (name: string) => { + this.handleOpenModal(name) + } + + handleAssignENSAddress = (ens: ENS) => { + const { onOpenModal } = this.props + onOpenModal('EnsMapAddressModal', { ens }) + } + handleOpenModal = (newName: string) => { const { onOpenModal } = this.props onOpenModal('UseAsAliasModal', { newName }) @@ -99,6 +124,54 @@ export default class ENSListPage extends React.PureComponent { ) } + renderLandLinkInfo(ens: ENS) { + if (!ens.landId) { + return ( + + ) + } + if (isCoords(ens.landId)) { + return ( +
+ + + {ens.landId} + + +
+ ) + } else { + return ( +
+ + + {`Estate (${ens.landId})`} + + +
+ ) + } + } + renderEnsList() { const { ensList, hasProfileCreated } = this.props const { page } = this.state @@ -106,7 +179,6 @@ export default class ENSListPage extends React.PureComponent { const total = ensList.length const totalPages = Math.ceil(total / PAGE_SIZE) const paginatedItems = this.paginate() - return ( <>
@@ -238,8 +310,142 @@ export default class ENSListPage extends React.PureComponent { ) } + renderNewEnsList() { + const { hasProfileCreated, ensList } = this.props + const { page } = this.state + + const total = ensList.length + const totalPages = Math.ceil(total / PAGE_SIZE) + const paginatedItems = this.paginate() + return ( +
+
+
+

{t('ens_list_page.title')}

+ {t('ens_list_page.result', { count: ensList.length })} +
+
+
+ {t('ens_list_page.sort_by')} {ensList.length > 1 ? this.renderSortDropdown() : null} +
+ +
+
+ + + + {t('ens_list_page.table.name')} + {t('ens_list_page.table.alias')} + {t('ens_list_page.table.address')} + {t('ens_list_page.table.land')} + {t('ens_list_page.table.actions')} + + + + {paginatedItems.map((ens: ENS, index) => { + return ( + + +
+ {ens.subdomain} + + {ens.name}.dcl.eth + + + + +
+
+ + {this.isAlias(ens) ? ( + + {this.props.avatar ? ( + {this.props.avatar.realName} + ) : ( + + )} + {ens.name} + {t('ens_list_page.table.you')} + + ) : ( + + )} + + + {ens.ensAddressRecord ? ( + + Ethereum + {getCroppedAddress(ens.ensAddressRecord)} + + + + + ) : ( + + )} + + {this.renderLandLinkInfo(ens)} + +
+ + + + +
+
+
+ ) + })} +
+
+ {totalPages > 1 && ( + this.setState({ page: +props.activePage! })} + /> + )} +
+ ) + } + render() { - const { isLoading, error } = this.props + const { isLoading, isEnsAddressEnabled, error } = this.props return ( { isLoading={isLoading} isPageFullscreen={true} > - {this.renderEnsList()} + {isEnsAddressEnabled ? this.renderNewEnsList() : this.renderEnsList()} ) } diff --git a/src/components/ENSListPage/ENSListPage.types.ts b/src/components/ENSListPage/ENSListPage.types.ts index 1ce6c5db9..19ae76227 100644 --- a/src/components/ENSListPage/ENSListPage.types.ts +++ b/src/components/ENSListPage/ENSListPage.types.ts @@ -2,6 +2,7 @@ import { Dispatch } from 'redux' import { ENS } from 'modules/ens/types' import { Land } from 'modules/land/types' import { openModal } from 'modules/modal/actions' +import { Avatar } from '@dcl/schemas' export enum SortBy { DESC = 'DESC', @@ -17,6 +18,8 @@ export type Props = { hasProfileCreated: boolean isLoggedIn: boolean isLoading: boolean + isEnsAddressEnabled: boolean + avatar: Avatar | null onNavigate: (path: string) => void onOpenModal: typeof openModal } @@ -28,7 +31,7 @@ export type State = { export type MapStateProps = Pick< Props, - 'address' | 'alias' | 'ensList' | 'lands' | 'hasProfileCreated' | 'isLoading' | 'error' | 'isLoggedIn' + 'address' | 'alias' | 'ensList' | 'lands' | 'hasProfileCreated' | 'isLoading' | 'error' | 'isLoggedIn' | 'isEnsAddressEnabled' | 'avatar' > export type MapDispatchProps = Pick export type MapDispatch = Dispatch diff --git a/src/components/ENSListPage/utils.spec.ts b/src/components/ENSListPage/utils.spec.ts new file mode 100644 index 000000000..8614c9355 --- /dev/null +++ b/src/components/ENSListPage/utils.spec.ts @@ -0,0 +1,21 @@ +import { getCroppedAddress } from './utils' + +describe('getCroppedAddress', () => { + describe('when address is a 42 character string', () => { + it('should return only the first 5 and last 6 characters of address', () => { + expect(getCroppedAddress('0xA4f689625F6F51AdF691988D38772BE8509087d2')).toEqual('0xA4f...9087d2') + }) + }) + + describe('when address is undefined', () => { + it('should return empty string', () => { + expect(getCroppedAddress(undefined)).toEqual('') + }) + }) + + describe('when address is not a 42 character string', () => { + it('should return empty string', () => { + expect(getCroppedAddress('0xA4f689625F6F51AdF691988D38772')).toEqual('') + }) + }) +}) diff --git a/src/components/ENSListPage/utils.ts b/src/components/ENSListPage/utils.ts new file mode 100644 index 000000000..581fafe30 --- /dev/null +++ b/src/components/ENSListPage/utils.ts @@ -0,0 +1,6 @@ +export function getCroppedAddress(address?: string) { + if (!address || address.length < 42) { + return '' + } + return `${address.slice(0, 5)}...${address.slice(-6)}` +} diff --git a/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.container.ts b/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.container.ts new file mode 100644 index 000000000..d5cc558f3 --- /dev/null +++ b/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.container.ts @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import { SET_ENS_ADDRESS_REQUEST, setENSAddressRequest } from 'modules/ens/actions' +import { isLoadingType } from 'decentraland-dapps/dist/modules/loading/selectors' +import { getError, getLoading, isWaitingTxSetAddress } from 'modules/ens/selectors' +import { RootState } from 'modules/common/types' +import { MapDispatch, MapDispatchProps, OwnProps } from './ENSMapAddressModal.types' +import EnsMapAddressModal from './ENSMapAddressModal' + +const mapState = (state: RootState) => { + const error = getError(state) + return { + isLoading: isLoadingType(getLoading(state), SET_ENS_ADDRESS_REQUEST) || isWaitingTxSetAddress(state), + error: error ? error.message : null + } +} + +const mapDispatch = (dispatch: MapDispatch, ownProps: OwnProps): MapDispatchProps => ({ + onSave: ((address: string) => dispatch(setENSAddressRequest(ownProps.metadata.ens, address))) as any +}) + +export default connect(mapState, mapDispatch)(EnsMapAddressModal) diff --git a/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.module.css b/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.module.css new file mode 100644 index 000000000..5c1ec189b --- /dev/null +++ b/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.module.css @@ -0,0 +1,49 @@ +.main { + padding: 30px 40px; + display: flex; + flex-direction: column; + gap: 30px; +} + +.info { + display: flex; + flex-direction: column; + gap: 15px; +} + +.network { + display: flex; + flex-direction: column; + gap: 10px; +} + +.actions { + display: flex; + width: 100%; + gap: 10px; +} + +.actions :global(.ui.button) { + flex: 1; +} + +.title { + font-size: 20px; + margin-bottom: 0; +} + +.description { + font-size: 14px; + line-height: 22px; +} + +.ethereum { + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; +} + +.ethereum img { + width: 24px; +} diff --git a/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.spec.tsx b/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.spec.tsx new file mode 100644 index 000000000..72774274a --- /dev/null +++ b/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.spec.tsx @@ -0,0 +1,61 @@ +import { renderWithProviders } from 'specs/utils' +import { Props } from './ENSMapAddressModal.types' +import EnsMapAddressModal from './ENSMapAddressModal' +import userEvent from '@testing-library/user-event' +import { RenderResult } from '@testing-library/react' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' + +function renderENSMapAddressModal(props: Partial = {}) { + return renderWithProviders() +} + +let screen: RenderResult + +describe('when clicking close button', () => { + it('should call onClose callback', () => { + const onClose = jest.fn() + renderENSMapAddressModal({ onClose }) + const closeBtn = document.querySelector('.dcl.close') + if (closeBtn) { + userEvent.click(closeBtn) + } + expect(onClose).toHaveBeenCalled() + }) +}) + +describe('when address is not defined', () => { + it('should disable save button', () => { + const screen = renderENSMapAddressModal() + expect(screen.getByRole('button', { name: t('ens_map_address_modal.save') })).toBeDisabled() + }) +}) + +describe('when address is defined', () => { + it('should call onSave when pressing save button', () => { + const onSave = jest.fn() + const screen = renderENSMapAddressModal({ onSave }) + const addressInput = screen.getByLabelText(t('ens_map_address_modal.address.label')) + userEvent.type(addressInput, '0xtestaddress') + const saveButton = screen.getByRole('button', { name: t('ens_map_address_modal.save') }) + userEvent.click(saveButton) + expect(onSave).toHaveBeenCalledWith('0xtestaddress') + }) +}) + +describe('when linking address is loading', () => { + beforeEach(() => { + screen = renderENSMapAddressModal({ isLoading: true }) + }) + it('should disable save button', () => { + expect(screen.getByRole('button', { name: t('ens_map_address_modal.save') })).toBeDisabled() + }) + + it('should disable address input', () => { + expect(screen.getByLabelText(t('ens_map_address_modal.address.label'))).toBeDisabled() + }) + + it('should not show close icon', () => { + const closeBtn = document.querySelector('.dcl.close') + expect(closeBtn).toBe(null) + }) +}) diff --git a/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.tsx b/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.tsx new file mode 100644 index 000000000..ad84127d2 --- /dev/null +++ b/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.tsx @@ -0,0 +1,63 @@ +import { useCallback, useEffect, useState } from 'react' +import { Button, Close, Field, Message } from 'decentraland-ui' +import Modal from 'decentraland-dapps/dist/containers/Modal' +import ethereumImg from '../../../icons/ethereum.svg' +import { Props } from './ENSMapAddressModal.types' +import styles from './ENSMapAddressModal.module.css' +import { t } from 'decentraland-dapps/dist/modules/translation/utils' + +export default function EnsMapAddressModal(props: Props) { + const { isLoading, error, onClose, onSave } = props + const [address, setAddress] = useState('') + + const handleSave = useCallback(() => { + onSave(address) + }, [address, onSave]) + + const handleAddressChange = useCallback( + (_evt, { value }) => { + setAddress(value) + }, + [setAddress] + ) + + useEffect(() => { + document.getElementById('address-input')?.focus() + }, []) + + return ( + } onClose={isLoading ? undefined : onClose} size="tiny"> +
+
+

{t('ens_map_address_modal.title')}

+ {t('ens_map_address_modal.description')} +
+
+ +
+ {t('ens_map_address_modal.network')} + + Ethereum + {t('ens_map_address_modal.ethereum')} + +
+
+ {error && } +
+ + +
+
+
+ ) +} diff --git a/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.types.ts b/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.types.ts new file mode 100644 index 000000000..297a211bc --- /dev/null +++ b/src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.types.ts @@ -0,0 +1,14 @@ +import { ModalProps } from 'decentraland-ui' +import { Dispatch } from 'redux' +import { SetENSAddressRequestAction } from 'modules/ens/actions' + +export type Props = ModalProps & { + error: string | null + isLoading: boolean + onSave: (address: string) => void +} + +export type MapStateProps = Pick +export type MapDispatchProps = Pick +export type MapDispatch = Dispatch +export type OwnProps = Omit diff --git a/src/components/Modals/ENSMapAddressModal/index.ts b/src/components/Modals/ENSMapAddressModal/index.ts new file mode 100644 index 000000000..6bd133624 --- /dev/null +++ b/src/components/Modals/ENSMapAddressModal/index.ts @@ -0,0 +1,2 @@ +import EnsMapAddressModal from './ENSMapAddressModal.container' +export default EnsMapAddressModal diff --git a/src/components/Modals/index.ts b/src/components/Modals/index.ts index 47b3fa574..1ce6b650b 100644 --- a/src/components/Modals/index.ts +++ b/src/components/Modals/index.ts @@ -50,3 +50,4 @@ export { default as EditVideoModal } from './EditVideoModal' export { default as EmotesV2AnnouncementModal } from './EmotesV2AnnouncementModal' export { default as WorldsYourStorageModal } from './WorldsYourStorageModal' export { default as WorldsForENSOwnersAnnouncementModal } from './WorldsForENSOwnersAnnouncementModal' +export { default as EnsMapAddressModal } from './ENSMapAddressModal' diff --git a/src/icons/ethereum.svg b/src/icons/ethereum.svg new file mode 100644 index 000000000..5c676a920 --- /dev/null +++ b/src/icons/ethereum.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/modules/ens/actions.ts b/src/modules/ens/actions.ts index 3bc881261..8395bf97a 100644 --- a/src/modules/ens/actions.ts +++ b/src/modules/ens/actions.ts @@ -9,7 +9,7 @@ export const FETCH_ENS_REQUEST = '[Request] Fetch ENS' export const FETCH_ENS_SUCCESS = '[Success] Fetch ENS' export const FETCH_ENS_FAILURE = '[Failure] Fetch ENS' -export const fetchENSRequest = (name: string, land: Land) => action(FETCH_ENS_REQUEST, { name, land }) +export const fetchENSRequest = (name: string, land?: Land) => action(FETCH_ENS_REQUEST, { name, land }) export const fetchENSSuccess = (ens: ENS) => action(FETCH_ENS_SUCCESS, { ens }) export const fetchENSFailure = (error: ENSError) => action(FETCH_ENS_FAILURE, { error }) @@ -74,6 +74,24 @@ export type SetENSContentRequestAction = ReturnType export type SetENSContentSuccessAction = ReturnType export type SetENSContentFailureAction = ReturnType +// set address to ens +export const SET_ENS_ADDRESS_REQUEST = '[Request] Set ENS Address' +export const SET_ENS_ADDRESS_SUCCESS = '[Success] Set ENS Address' +export const SET_ENS_ADDRESS_FAILURE = '[Failure] Set ENS Address' + +export const setENSAddressRequest = (ens: ENS, address: string) => action(SET_ENS_ADDRESS_REQUEST, { ens, address }) +export const setENSAddressSuccess = (ens: ENS, address: string, chainId: ChainId, txHash: string) => + action(SET_ENS_ADDRESS_SUCCESS, { + ...buildTransactionPayload(chainId, txHash, { ens, address }), + ens, + address + }) +export const setENSAddressFailure = (ens: ENS, address: string, error: ENSError) => action(SET_ENS_ADDRESS_FAILURE, { ens, address, error }) + +export type SetENSAddressRequestAction = ReturnType +export type SetENSAddressSuccessAction = ReturnType +export type SetENSAddressFailureAction = ReturnType + // Get a ENS List (list of names) owned by the current account export const FETCH_ENS_LIST_REQUEST = '[Request] Fetch ENS List' export const FETCH_ENS_LIST_SUCCESS = '[Success] Fetch ENS List' diff --git a/src/modules/ens/reducer.spec.ts b/src/modules/ens/reducer.spec.ts index 17e80212c..2c6ff3a0d 100644 --- a/src/modules/ens/reducer.spec.ts +++ b/src/modules/ens/reducer.spec.ts @@ -1,11 +1,22 @@ -import { FETCH_EXTERNAL_NAMES_REQUEST, fetchExternalNamesFailure, fetchExternalNamesRequest, fetchExternalNamesSuccess } from './actions' +import { ChainId } from '@dcl/schemas' +import { + FETCH_EXTERNAL_NAMES_REQUEST, + SET_ENS_ADDRESS_SUCCESS, + fetchExternalNamesFailure, + fetchExternalNamesRequest, + fetchExternalNamesSuccess, + setENSAddressFailure, + setENSAddressRequest +} from './actions' import { ENSState, INITIAL_STATE, ensReducer } from './reducer' import { ENS, ENSError } from './types' +import { fetchTransactionSuccess } from 'decentraland-dapps/dist/modules/transaction/actions' +import { Transaction } from 'decentraland-dapps/dist/modules/transaction/types' let state: ENSState beforeEach(() => { - state = INITIAL_STATE + state = { ...INITIAL_STATE } }) describe('when handling the fetch external names actions', () => { @@ -86,3 +97,92 @@ describe('when handling the fetch external names actions', () => { }) }) }) + +describe('when handling set ens address actions', () => { + let ens: ENS + let address: string + + beforeEach(() => { + state = { ...INITIAL_STATE } + address = '0xtest' + ens = { + name: 'test', + subdomain: 'test.dcl.eth', + nftOwnerAddress: address + } as ENS + }) + + describe('when handling the set ens address request action', () => { + it('should add the set ens address request action to the loading state', () => { + const action = setENSAddressRequest(ens, address) + const newState = ensReducer(state, action) + expect(newState.loading[0]).toEqual(action) + }) + + it('should set the error as null', () => { + const action = setENSAddressRequest(ens, address) + const newState = ensReducer({ ...state, error: { message: 'some error' } }, action) + expect(newState.error).toEqual(null) + }) + }) + + describe('when handling the set ens address failure action', () => { + let error: ENSError + beforeEach(() => { + error = { message: 'some error' } + state = { + ...state, + loading: [setENSAddressRequest(ens, address)] + } + }) + + it('should remove the set ens address request action from the loading state', () => { + const action = setENSAddressFailure(ens, address, error) + const newState = ensReducer(state, action) + expect(newState.loading.length).toEqual(0) + }) + + it('should add the error to the state', () => { + const action = setENSAddressFailure(ens, address, error) + const newState = ensReducer(state, action) + expect(newState.error).toEqual(error) + }) + }) + + describe('when handling the set ens address success action', () => { + let action: ReturnType + beforeEach(() => { + state = { + ...state, + data: { + 'test.dcl.eth': { + subdomain: 'test.dcl.eth', + name: 'test' + } as ENS + }, + error: { message: 'some error' }, + loading: [setENSAddressRequest(ens, address)] + } + + action = fetchTransactionSuccess({ + actionType: SET_ENS_ADDRESS_SUCCESS, + payload: { ens, address, chainId: ChainId.ETHEREUM_SEPOLIA, hash: 'hash' } + } as Transaction) + }) + + it('should remove the set ens address request action from the loading state', () => { + const newState = ensReducer(state, action) + expect(newState.loading.length).toEqual(0) + }) + + it('should set the error as null', () => { + const newState = ensReducer(state, action) + expect(newState.error).toEqual(null) + }) + + it('should set the address to the subdomain', () => { + const newState = ensReducer(state, action) + expect(newState.data['test.dcl.eth'].ensAddressRecord).toEqual(address) + }) + }) +}) diff --git a/src/modules/ens/reducer.ts b/src/modules/ens/reducer.ts index c92572c20..9b89d5e83 100644 --- a/src/modules/ens/reducer.ts +++ b/src/modules/ens/reducer.ts @@ -62,7 +62,14 @@ import { FetchExternalNamesRequestAction, FetchExternalNamesSuccessAction, FetchExternalNamesFailureAction, - FETCH_EXTERNAL_NAMES_SUCCESS + FETCH_EXTERNAL_NAMES_SUCCESS, + SET_ENS_ADDRESS_REQUEST, + SET_ENS_ADDRESS_SUCCESS, + SET_ENS_ADDRESS_FAILURE, + SetENSAddressRequestAction, + SetENSAddressFailureAction, + SetENSAddressSuccessAction, + setENSAddressSuccess } from './actions' import { ENS, ENSError, Authorization } from './types' import { isExternalName } from './utils' @@ -116,6 +123,9 @@ export type ENSReducerAction = | FetchExternalNamesRequestAction | FetchExternalNamesSuccessAction | FetchExternalNamesFailureAction + | SetENSAddressRequestAction + | SetENSAddressSuccessAction + | SetENSAddressFailureAction export function ensReducer(state: ENSState = INITIAL_STATE, action: ENSReducerAction): ENSState { switch (action.type) { @@ -131,7 +141,8 @@ export function ensReducer(state: ENSState = INITIAL_STATE, action: ENSReducerAc case SET_ENS_RESOLVER_SUCCESS: case ALLOW_CLAIM_MANA_REQUEST: case ALLOW_CLAIM_MANA_SUCCESS: - case FETCH_EXTERNAL_NAMES_REQUEST: { + case FETCH_EXTERNAL_NAMES_REQUEST: + case SET_ENS_ADDRESS_REQUEST: { return { ...state, error: null, @@ -259,7 +270,8 @@ export function ensReducer(state: ENSState = INITIAL_STATE, action: ENSReducerAc case FETCH_ENS_LIST_FAILURE: case FETCH_ENS_AUTHORIZATION_FAILURE: case ALLOW_CLAIM_MANA_FAILURE: - case FETCH_EXTERNAL_NAMES_FAILURE: { + case FETCH_EXTERNAL_NAMES_FAILURE: + case SET_ENS_ADDRESS_FAILURE: { return { ...state, loading: loadingReducer(state.loading, action), @@ -285,6 +297,21 @@ export function ensReducer(state: ENSState = INITIAL_STATE, action: ENSReducerAc } } } + case SET_ENS_ADDRESS_SUCCESS: { + const { ens, address, chainId, txHash } = transaction.payload + return { + ...state, + loading: loadingReducer(state.loading, setENSAddressSuccess(ens, address, chainId, txHash)), + error: null, + data: { + ...state.data, + [ens.subdomain]: { + ...state.data[ens.subdomain], + ensAddressRecord: address + } + } + } + } case SET_ENS_CONTENT_SUCCESS: { const { ens, content, land } = transaction.payload const { subdomain } = ens diff --git a/src/modules/ens/sagas.spec.ts b/src/modules/ens/sagas.spec.ts index 96a5b62ef..6bd5789a6 100644 --- a/src/modules/ens/sagas.spec.ts +++ b/src/modules/ens/sagas.spec.ts @@ -1,14 +1,25 @@ import * as matchers from 'redux-saga-test-plan/matchers' import { throwError } from 'redux-saga-test-plan/providers' import { expectSaga } from 'redux-saga-test-plan' +import { namehash } from '@ethersproject/hash' import { call, select } from 'redux-saga/effects' -import { ethers } from 'ethers' +import { Signer, ethers } from 'ethers' import { BuilderClient } from '@dcl/builder-client' import { ChainId, Network } from '@dcl/schemas' -import { ERC20__factory, ERC20, DCLController__factory, DCLRegistrar__factory, ENS__factory } from 'contracts' +import { + ERC20__factory, + ERC20, + DCLController__factory, + DCLRegistrar__factory, + ENS__factory, + ENSResolver__factory, + ENSResolver +} from 'contracts' import { getChainIdByNetwork, getSigner } from 'decentraland-dapps/dist/lib/eth' import { getAddress } from 'decentraland-dapps/dist/modules/wallet/selectors' import { connectWalletSuccess } from 'decentraland-dapps/dist/modules/wallet/actions' +import { waitForTx } from 'decentraland-dapps/dist/modules/transaction/utils' +import { closeModal } from 'modules/modal/actions' import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types' import { CONTROLLER_V2_ADDRESS, ENS_ADDRESS, MANA_ADDRESS, REGISTRAR_ADDRESS } from 'modules/common/contracts' import { fetchWorldDeploymentsRequest } from 'modules/deployment/actions' @@ -29,10 +40,13 @@ import { fetchENSWorldStatusSuccess, fetchExternalNamesFailure, fetchExternalNamesRequest, - fetchExternalNamesSuccess + fetchExternalNamesSuccess, + setENSAddressFailure, + setENSAddressRequest, + setENSAddressSuccess } from './actions' import { ensSaga } from './sagas' -import { ENS, ENSError, WorldStatus } from './types' +import { ENS, ENSError, ENSOrigin, WorldStatus } from './types' import { getENSBySubdomain, getExternalNames } from './selectors' import { addWorldStatusToEachENS } from './utils' @@ -43,6 +57,7 @@ const MockBuilderClient = BuilderClient as jest.MockedClass { resolver: jest.fn().mockResolvedValue(ethers.constants.AddressZero), owner: jest.fn().mockResolvedValue('address') } as unknown as ENS__factory + ensResolverContract = { + 'setAddr(bytes32,address)': jest.fn().mockResolvedValue(''), + 'addr(bytes32)': jest.fn().mockResolvedValue('0xaddr') + } as unknown as ENSResolver }) describe('when handling the approve claim mana request', () => { @@ -130,6 +149,7 @@ describe('when handling the claim name request', () => { beforeEach(() => { address = '0xanAddress' jest.spyOn(WorldsAPI.prototype, 'fetchWorld').mockResolvedValue(null) + ENSResolver__factory.connect = jest.fn().mockReturnValue(ensResolverContract) }) afterEach(() => { jest.restoreAllMocks() @@ -145,7 +165,8 @@ describe('when handling the claim name request', () => { resolver: '0x0000000000000000000000000000000000000000', content: '', landId: undefined, - worldStatus: null + worldStatus: null, + ensAddressRecord: '0xaddr' } const ENSList: ENS[] = validDomains.map(domain => ({ name: domain, @@ -367,3 +388,49 @@ describe('when handling the wallet connection', () => { .silentRun() }) }) + +describe('when handling the set ens address request', () => { + let signer: Signer + let address: string + let ens: ENS + let hash: string + + beforeEach(() => { + ens = { + subdomain: 'test.dcl.eth', + name: 'test' + } as ENS + signer = {} as Signer + address = '0xtest' + hash = 'tx-hash' + ENSResolver__factory.connect = jest.fn().mockReturnValue(ensResolverContract) + }) + + it('should call resolver contract with the ens domain and address', () => { + return expectSaga(ensSaga, builderClient, ensApi) + .provide([ + [call(getWallet), { address: 'address', chainId: ChainId.ETHEREUM_GOERLI }], + [call(getSigner), { signer }], + [call([ensResolverContract, 'setAddr(bytes32,address)'], namehash(ens.subdomain), address), { hash } as ethers.ContractTransaction], + [call(waitForTx, hash), true] + ]) + .put(setENSAddressSuccess(ens, address, ChainId.ETHEREUM_GOERLI, hash)) + .put(closeModal('EnsMapAddressModal')) + .dispatch(setENSAddressRequest(ens, address)) + .silentRun() + }) + + it('should put the failure action when something goes wrong', () => { + const error = { message: 'an error message', code: 1, name: 'error' } + return expectSaga(ensSaga, builderClient, ensApi) + .provide([ + [call(getWallet), { address: 'address', chainId: ChainId.ETHEREUM_GOERLI }], + [call(getSigner), { signer }], + [call([ensResolverContract, 'setAddr(bytes32,address)'], namehash(ens.subdomain), address), throwError(error)], + [call(waitForTx, hash), true] + ]) + .put(setENSAddressFailure(ens, address, { message: error.message, code: error.code, origin: ENSOrigin.ADDRESS })) + .dispatch(setENSAddressRequest(ens, address)) + .silentRun() + }) +}) diff --git a/src/modules/ens/sagas.ts b/src/modules/ens/sagas.ts index ee4c83910..0acbc8d4a 100644 --- a/src/modules/ens/sagas.ts +++ b/src/modules/ens/sagas.ts @@ -74,7 +74,11 @@ import { FetchExternalNamesRequestAction, fetchExternalNamesSuccess, fetchExternalNamesFailure, - fetchExternalNamesRequest + fetchExternalNamesRequest, + SetENSAddressRequestAction, + setENSAddressSuccess, + setENSAddressFailure, + SET_ENS_ADDRESS_REQUEST } from './actions' import { getENSBySubdomain, getExternalNames } from './selectors' import { ENS, ENSOrigin, ENSError, Authorization } from './types' @@ -93,6 +97,7 @@ export function* ensSaga(builderClient: BuilderClient, ensApi: ENSApi) { yield takeEvery(RECLAIM_NAME_REQUEST, handleReclaimNameRequest) yield takeEvery(FETCH_EXTERNAL_NAMES_REQUEST, handleFetchExternalNamesRequest) yield takeEvery(CONNECT_WALLET_SUCCESS, handleConnectWallet) + yield takeEvery(SET_ENS_ADDRESS_REQUEST, handleSetENSAddressRequest) function* handleFetchLandsSuccess() { yield put(fetchENSAuthorizationRequest()) @@ -117,6 +122,8 @@ export function* ensSaga(builderClient: BuilderClient, ensApi: ENSApi) { const owner = ownerAddress.toLowerCase() const tokenId = nftTokenId.toString() + const ensResolverContract = ENSResolver__factory.connect(ENS_RESOLVER_ADDRESS, signer) + const ensAddressRecord: string = yield call([ensResolverContract, 'addr(bytes32)'], nodehash) if (resolverAddress.toString() === ethers.constants.AddressZero) { yield put( @@ -127,54 +134,61 @@ export function* ensSaga(builderClient: BuilderClient, ensApi: ENSApi) { nftOwnerAddress: address, subdomain, resolver: ethers.constants.AddressZero, - content: ethers.constants.AddressZero + content: ethers.constants.AddressZero, + ensAddressRecord }) ) return } - const resolverContract = ENSResolver__factory.connect(resolverAddress, signer) + let currentContent = '' - const [x, y] = getCenter(getSelection(land)) + if (land) { + const resolverContract = ENSResolver__factory.connect(resolverAddress, signer) - const { ipfsHash, contentHash }: LandHashes = yield call( - [builderClient, 'createLandRedirectionFile'], - { x, y }, - getCurrentLocale().locale - ) + const [x, y] = getCenter(getSelection(land)) - const currentContent: string = yield call([resolverContract, 'contenthash'], nodehash) - if (currentContent === ethers.constants.AddressZero) { - yield put( - fetchENSSuccess({ - name, - tokenId, - ensOwnerAddress: owner, - nftOwnerAddress: address, - subdomain, - resolver: resolverAddress.toString(), - content: ethers.constants.AddressZero, - ipfsHash - }) + const { ipfsHash, contentHash }: LandHashes = yield call( + [builderClient, 'createLandRedirectionFile'], + { x, y }, + getCurrentLocale().locale ) - return - } - if (`0x${contentHash}` === currentContent) { - yield put( - fetchENSSuccess({ - name, - tokenId, - ensOwnerAddress: owner, - nftOwnerAddress: address, - subdomain, - resolver: ENS_RESOLVER_ADDRESS, - content: contentHash, - ipfsHash, - landId: land.id - }) - ) - return + currentContent = yield call([resolverContract, 'contenthash'], nodehash) + if (currentContent === ethers.constants.AddressZero) { + yield put( + fetchENSSuccess({ + name, + tokenId, + ensOwnerAddress: owner, + nftOwnerAddress: address, + subdomain, + resolver: resolverAddress.toString(), + content: ethers.constants.AddressZero, + ipfsHash, + ensAddressRecord + }) + ) + return + } + + if (`0x${contentHash}` === currentContent) { + yield put( + fetchENSSuccess({ + name, + tokenId, + ensOwnerAddress: owner, + nftOwnerAddress: address, + subdomain, + resolver: ENS_RESOLVER_ADDRESS, + content: contentHash, + ipfsHash, + landId: land.id, + ensAddressRecord + }) + ) + return + } } yield put( @@ -186,7 +200,8 @@ export function* ensSaga(builderClient: BuilderClient, ensApi: ENSApi) { subdomain, resolver: ENS_RESOLVER_ADDRESS, content: currentContent ?? ethers.constants.AddressZero, - landId: '' + landId: '', + ensAddressRecord }) ) } catch (error) { @@ -304,6 +319,25 @@ export function* ensSaga(builderClient: BuilderClient, ensApi: ENSApi) { } } + function* handleSetENSAddressRequest(action: SetENSAddressRequestAction) { + const { ens, address } = action.payload + try { + const wallet: Wallet = yield call(getWallet) + const signer: ethers.Signer = yield call(getSigner) + const nodehash = namehash(ens.subdomain) + const resolverContract = ENSResolver__factory.connect(ENS_RESOLVER_ADDRESS, signer) + + const transaction: ethers.ContractTransaction = yield call([resolverContract, 'setAddr(bytes32,address)'], nodehash, address) + + yield put(setENSAddressSuccess(ens, address, wallet.chainId, transaction.hash)) + yield call(waitForTx, transaction.hash) + yield put(closeModal('EnsMapAddressModal')) + } catch (error) { + const ensError: ENSError = { message: error.message, code: error.code, origin: ENSOrigin.ADDRESS } + yield put(setENSAddressFailure(ens, address, ensError)) + } + } + function* handleFetchAuthorizationRequest(_action: FetchENSAuthorizationRequestAction) { try { const from: string = yield select(getAddress) @@ -377,6 +411,7 @@ export function* ensSaga(builderClient: BuilderClient, ensApi: ENSApi) { let landId: string | undefined = undefined let content = '' let worldStatus = null + let ensAddressRecord = '' const nodehash = namehash(subdomain) const [resolverAddress, owner, tokenId]: [string, string, string] = await Promise.all([ @@ -386,6 +421,14 @@ export function* ensSaga(builderClient: BuilderClient, ensApi: ENSApi) { ]) const resolver = resolverAddress.toString() + try { + const resolverContract = ENSResolver__factory.connect(ENS_RESOLVER_ADDRESS, signer) + const resolvedAddress = await resolverContract['addr(bytes32)'](nodehash) + ensAddressRecord = resolvedAddress !== ethers.constants.AddressZero ? resolvedAddress : '' + } catch (e) { + console.error('Failed to fetch ens address record') + } + if (resolver !== ethers.constants.AddressZero) { try { const resolverContract = ENSResolver__factory.connect(resolverAddress, signer) @@ -426,6 +469,7 @@ export function* ensSaga(builderClient: BuilderClient, ensApi: ENSApi) { subdomain, resolver, content, + ensAddressRecord, landId, worldStatus } diff --git a/src/modules/ens/selectors.spec.ts b/src/modules/ens/selectors.spec.ts index 5df800da2..2f6087244 100644 --- a/src/modules/ens/selectors.spec.ts +++ b/src/modules/ens/selectors.spec.ts @@ -1,6 +1,9 @@ import { RootState } from 'modules/common/types' -import { getExternalNames, getExternalNamesForConnectedWallet, getExternalNamesForWallet } from './selectors' +import { TransactionState } from 'decentraland-dapps/dist/modules/transaction/reducer' +import { Transaction, TransactionStatus } from 'decentraland-dapps/dist/modules/transaction/types' +import { getExternalNames, getExternalNamesForConnectedWallet, getExternalNamesForWallet, isWaitingTxSetAddress } from './selectors' import { ENSState } from './reducer' +import { SET_ENS_ADDRESS_SUCCESS, SET_ENS_CONTENT_SUCCESS } from './actions' let state: RootState let wallet1: string @@ -109,3 +112,75 @@ describe('when getting the external names for a provided wallet', () => { }) }) }) + +describe('when using isWaitingTxSetAddress selector', () => { + describe('and there are no pending transactions', () => { + beforeEach(() => { + state = { + transaction: { data: [], loading: [], error: null } as TransactionState, + wallet: { + data: { + address: wallet1 + } + } + } as RootState + }) + it('should return false', () => { + expect(isWaitingTxSetAddress(state)).toEqual(false) + }) + }) + + describe('and there are pending transactions', () => { + describe('and the transaction actionType is SET_ENS_ADDRESS_SUCCESS', () => { + beforeEach(() => { + state = { + transaction: { + data: [ + { + actionType: SET_ENS_ADDRESS_SUCCESS, + status: TransactionStatus.PENDING, + from: wallet1 + } as Transaction + ], + loading: [], + error: null + } as TransactionState, + wallet: { + data: { + address: wallet1 + } + } + } as RootState + }) + it('should return true', () => { + expect(isWaitingTxSetAddress(state)).toEqual(true) + }) + }) + + describe('and the transaction actionType is not SET_ENS_ADDRESS_SUCCESS', () => { + beforeEach(() => { + state = { + transaction: { + data: [ + { + actionType: SET_ENS_CONTENT_SUCCESS, + status: TransactionStatus.PENDING, + from: wallet1 + } as Transaction + ], + loading: [], + error: null + } as TransactionState, + wallet: { + data: { + address: wallet1 + } + } + } as RootState + }) + it('should return false', () => { + expect(isWaitingTxSetAddress(state)).toEqual(false) + }) + }) + }) +}) diff --git a/src/modules/ens/selectors.ts b/src/modules/ens/selectors.ts index 5390b321f..1e73210e7 100644 --- a/src/modules/ens/selectors.ts +++ b/src/modules/ens/selectors.ts @@ -15,7 +15,8 @@ import { ALLOW_CLAIM_MANA_SUCCESS, RECLAIM_NAME_SUCCESS, CLAIM_NAME_REQUEST, - CLAIM_NAME_TRANSACTION_SUBMITTED + CLAIM_NAME_TRANSACTION_SUBMITTED, + SET_ENS_ADDRESS_SUCCESS } from './actions' import { Authorization, ENS } from './types' import { ENSState } from './reducer' @@ -95,6 +96,10 @@ export const isWaitingTxSetResolver = createSelector SET_ENS_RESOLVER_SUCCESS === transaction.actionType) ) +export const isWaitingTxSetAddress = createSelector(getPendingTransactions, transactions => + transactions.some(transaction => SET_ENS_ADDRESS_SUCCESS === transaction.actionType) +) + export const isWaitingTxSetLandContent = (state: RootState, landId: string) => getPendingTransactions(state).some( transaction => SET_ENS_CONTENT_SUCCESS === transaction.actionType && transaction.payload.land.id === landId diff --git a/src/modules/ens/types.ts b/src/modules/ens/types.ts index fbb8b1c49..6fa4fb475 100644 --- a/src/modules/ens/types.ts +++ b/src/modules/ens/types.ts @@ -20,6 +20,8 @@ export type ENS = { landId?: string worldStatus?: WorldStatus | null + + ensAddressRecord?: string } export type ENSError = { @@ -30,7 +32,8 @@ export type ENSError = { export enum ENSOrigin { RESOLVER = 'Resolver', - CONTENT = 'Content' + CONTENT = 'Content', + ADDRESS = 'Address' } export type Authorization = { diff --git a/src/modules/features/selectors.spec.ts b/src/modules/features/selectors.spec.ts index ea34a42d2..c04c47c77 100644 --- a/src/modules/features/selectors.spec.ts +++ b/src/modules/features/selectors.spec.ts @@ -7,7 +7,8 @@ import { getIsMaintenanceEnabled, getIsSDK7TemplatesEnabled, getIsWorldsForEnsOwnersEnabled, - getIsNavbarV2Enabled + getIsNavbarV2Enabled, + getIsEnsAddressEnabled } from './selectors' import { FeatureName } from './types' @@ -65,7 +66,8 @@ const ffSelectors = [ { selector: getIsSDK7TemplatesEnabled, app: ApplicationName.BUILDER, feature: FeatureName.SDK7_TEMPLATES }, { selector: getIsCreateSceneOnlySDK7Enabled, app: ApplicationName.BUILDER, feature: FeatureName.CREATE_SCENE_ONLY_SDK7 }, { selector: getIsAuthDappEnabled, app: ApplicationName.DAPPS, feature: FeatureName.AUTH_DAPP }, - { selector: getIsNavbarV2Enabled, app: ApplicationName.DAPPS, feature: FeatureName.NAVBAR_V2 } + { selector: getIsNavbarV2Enabled, app: ApplicationName.DAPPS, feature: FeatureName.NAVBAR_V2 }, + { selector: getIsEnsAddressEnabled, app: ApplicationName.DAPPS, feature: FeatureName.ENS_ADDRESS } ] ffSelectors.forEach(({ selector, app, feature }) => { diff --git a/src/modules/features/selectors.ts b/src/modules/features/selectors.ts index e90b80aab..8a002e30c 100644 --- a/src/modules/features/selectors.ts +++ b/src/modules/features/selectors.ts @@ -85,3 +85,11 @@ export const getIsNavbarV2Enabled = (state: RootState) => { return false } } + +export const getIsEnsAddressEnabled = (state: RootState) => { + try { + return getIsFeatureEnabled(state, ApplicationName.DAPPS, FeatureName.ENS_ADDRESS) + } catch (e) { + return false + } +} diff --git a/src/modules/features/types.ts b/src/modules/features/types.ts index c189c8517..502cab6d2 100644 --- a/src/modules/features/types.ts +++ b/src/modules/features/types.ts @@ -10,5 +10,6 @@ export enum FeatureName { SDK7_TEMPLATES = 'sdk7-templates', CREATE_SCENE_ONLY_SDK7 = 'create-scene-only-sdk7', AUTH_DAPP = 'auth-dapp', - NAVBAR_V2 = 'navbar2_variant' + NAVBAR_V2 = 'navbar2_variant', + ENS_ADDRESS = 'ens-address' } diff --git a/src/modules/location/selectors.spec.ts b/src/modules/location/selectors.spec.ts index d5fe7bad8..994834dfd 100644 --- a/src/modules/location/selectors.spec.ts +++ b/src/modules/location/selectors.spec.ts @@ -2,7 +2,7 @@ import { getData } from 'decentraland-dapps/dist/modules/wallet/selectors' import { RootState } from 'modules/common/types' import { Item, ItemType } from 'modules/item/types' import { locations } from 'routing/locations' -import { getCollectionId, getSelectedItemId, getTemplateId } from './selectors' +import { getCollectionId, getSelectedItemId, getTemplateId, getENSName } from './selectors' jest.mock('decentraland-dapps/dist/modules/wallet/selectors') @@ -323,3 +323,19 @@ describe('when getting the selected item id using the current url', () => { }) }) }) + +describe('when getting the name from the current url', () => { + it('should return the name section of the url', () => { + const name = 'test' + const mockState = { + router: { + action: 'POP', + location: { + pathname: locations.ensDetail(name) + } + } + } as unknown + + expect(getENSName(mockState as RootState)).toEqual(name) + }) +}) diff --git a/src/modules/location/selectors.ts b/src/modules/location/selectors.ts index 078928d30..78c9af075 100644 --- a/src/modules/location/selectors.ts +++ b/src/modules/location/selectors.ts @@ -85,3 +85,15 @@ export const getSelectedCollectionId = (state: RootState) => new URLSearchParams export const isReviewing = (state: RootState) => !!new URLSearchParams(getSearch(state)).get('reviewing') export const getState = (state: RootState) => state.location export const hasHistory = (state: RootState) => getState(state).hasHistory + +export const ensNameMatchSelector = createMatchSelector< + RootState, + { + name: string + } +>(locations.ensDetail()) + +export const getENSName = (state: RootState) => { + const result = ensNameMatchSelector(state) + return result ? result.params.name : null +} diff --git a/src/modules/translation/languages/en.json b/src/modules/translation/languages/en.json index a316820d0..618a9f9ce 100644 --- a/src/modules/translation/languages/en.json +++ b/src/modules/translation/languages/en.json @@ -577,23 +577,59 @@ "subtitle": "Your are editing {name}." }, "ens_list_page": { + "title": "NAMEs", + "sort_by": "Sort by", + "mint_name": "Mint name", + "result": "{count} {count, plural, one {result} other {results}}", + "edit": "Edit", + "transfer": "Transfer", "table": { "name": "Name", "link": "Link", - "assigned_to": "Assigned To" + "assigned_to": "Assigned To", + "address": "Address", + "alias": "Alias", + "land": "Land", + "actions": "Actions", + "you": "you" }, "assigned_to_land": "Land ({landId})", "assigned_to_estate": "Estate ({landId})", "button": { "assign": "Use as Link", "edit": "Edit Link", - "use_as_alias": "Use as Alias" + "use_as_alias": "Use as Alias", + "add_to_avatar": "Add to Avatar", + "link_to_address": "Link to an Ethereum Address", + "assign_to_land": "Assign to Land or World" }, "empty_names": "It looks like you don't have any Names.{br} {link} to get started.", "items": "{count} {count, plural, one {result} other {results}}", "alias_popup": "This name is being used by your profile but it can still be assigned", "not_profile_created": "You need to get into the world first to set an alias." }, + "ens_detail_page": { + "name": "Name", + "address": "Address", + "alias": "Alias", + "land": "Land", + "unassign": "Unassigned", + "set_as_primary": "Set as primary", + "edit_address": "Edit address" + }, + "ens_map_address_modal": { + "title": "Link to an Ethereum Address", + "description": "Enable others to reference your account by defining an Ethereum address with your Decentraland NAME. This allows you to receive assets simply by providing your NAME.", + "address": { + "label": "Linked Ethereum Address", + "placeholder": "Address" + }, + "network": "Network", + "ethereum": "Ethereum Mainnet Network", + "error_title": "An error occur while setting the address", + "learn_more": "Learn more", + "save": "Save" + }, "worlds_list_page": { "table": { "name": "NAME", diff --git a/src/modules/translation/languages/es.json b/src/modules/translation/languages/es.json index 28414117f..4399f024a 100644 --- a/src/modules/translation/languages/es.json +++ b/src/modules/translation/languages/es.json @@ -579,23 +579,59 @@ "unpublish_needed": "Por favor Despublica tu Escena antes de actualizar los detalles de la misma" }, "ens_list_page": { + "title": "Nombres", + "sort_by": "Ordernar por", + "mint_name": "Comprar nombre", + "result": "{count} {count, plural, one {resultado} other {resultados}}", + "edit": "Editar", + "transfer": "Transferir", "table": { "name": "Nombre", "link": "Enlace", - "assigned_to": "Asignado a" + "assigned_to": "Asignado a", + "address": "Dirección", + "alias": "alias", + "land": "Tierra", + "actions": "Acciones", + "you": "tú" }, "assigned_to_land": "Tierra ({landId})", "assigned_to_estate": "Estate ({landId})", "button": { "assign": "Usar como link", "edit": "Editar link", - "use_as_alias": "Usar como alias" + "use_as_alias": "Usar como alias", + "add_to_avatar": "Agregar a Avatar", + "link_to_address": "Enlazar a una dirección de Ethereum", + "assign_to_land": "Asignar a tierra o mundo" }, "empty_names": "No tienes ningun nombre aún.{br} {link} para comenzar.", "items": "{count} {count, plural, one {resultado} other {resultados}}", "alias_popup": "Este nombre está siendo usado por tu perfil pero igual puede ser asignado", "not_profile_created": "Primero necesitas entrar en el mundo antes de asignar un alias." }, + "ens_detail_page": { + "name": "Nombre", + "address": "Dirección", + "alias": "Alias", + "land": "Tierra", + "unassign": "Sin asignar", + "set_as_primary": "Usar como alias", + "edit_address": "Editar dirección" + }, + "ens_map_address_modal": { + "title": "Enlace a una dirección de Ethereum", + "description": "Permita que otros hagan referencia a su cuenta definiendo una dirección de Ethereum con su nombre Decentraland. Esto le permite recibir activos simplemente proporcionando su nombre.", + "address": { + "label": "Dirección de Ethereum vinculada", + "placeholder": "DIRECCIÓN" + }, + "network": "Red", + "ethereum": "Ethereum Mainnet", + "error_title": "Se produjo un error al configurar la dirección", + "learn_more": "Aprende más", + "save": "Guardar" + }, "worlds_list_page": { "table": { "name": "NOMBRE", diff --git a/src/modules/translation/languages/zh.json b/src/modules/translation/languages/zh.json index f592408fd..4090324fc 100644 --- a/src/modules/translation/languages/zh.json +++ b/src/modules/translation/languages/zh.json @@ -581,23 +581,59 @@ "title": "糟糕!" }, "ens_list_page": { + "title": "名称", + "sort_by": "排序方式", + "mint_name": "薄荷名称", + "result": "{count} {count, plural, one {结果} other {结果}}", + "edit": "编辑", + "transfer": "转移", "table": { "name": "名称", "link": "链接", - "assigned_to": "分配给" + "assigned_to": "分配给", + "address": "地址", + "alias": "别名", + "land": "土地", + "actions": "动作", + "you": "你" }, "assigned_to_land": "土地 ({landId})", "assigned_to_estate": "州 ({landId})", "button": { "assign": "分配", "edit": "修", - "use_as_alias": "用作别名" + "use_as_alias": "用作别名", + "add_to_avatar": "添加到头像", + "link_to_address": "链接到以太坊地址", + "assign_to_land": "分配土地或世界" }, "empty_names": "看来您没有任何名称。{br} {link}开始使用。", "items": "{count}结果", "alias_popup": "您的个人资料正在使用此名称,但仍可以分配", "not_profile_created": "您需要先进入世界才能设置别名。" }, + "ens_detail_page": { + "name": "姓名", + "address": "地址", + "alias": "别名", + "land": "土地", + "unassign": "未分配", + "set_as_primary": "设置为主要", + "edit_address": "编辑地址" + }, + "ens_map_address_modal": { + "title": "链接到以太坊地址", + "description": "通过定义您的分散名称来定义以太坊地址,使其他人可以参考您的帐户。这使您只需提供姓名即可获得资产。", + "address": { + "label": "链接的以太坊地址", + "placeholder": "地址" + }, + "network": "网络", + "ethereum": "以太坊主网网络", + "error_title": "设置地址时发生错误", + "learn_more": "了解更多", + "save": "节省" + }, "worlds_list_page": { "table": { "name": "姓名", diff --git a/src/routing/Routes.tsx b/src/routing/Routes.tsx index 53d27b9ee..1843749f4 100644 --- a/src/routing/Routes.tsx +++ b/src/routing/Routes.tsx @@ -30,6 +30,7 @@ const LandDetailPage = React.lazy(() => import('components/LandDetailPage')) const LandTransferPage = React.lazy(() => import('components/LandTransferPage')) const LandEditPage = React.lazy(() => import('components/LandEditPage')) const ENSListPage = React.lazy(() => import('components/ENSListPage')) +const ENSDetailPage = React.lazy(() => import('components/ENSDetailPage')) const WorldListPage = React.lazy(() => import('components/WorldListPage')) const WorldListPageWorldsForEnsOwners = React.lazy(() => import('components/WorldListPage_WorldsForEnsOwnersFeature')) const LandSelectENSPage = React.lazy(() => import('components/LandSelectENSPage')) @@ -126,6 +127,7 @@ export default class Routes extends React.Component { , + , , diff --git a/src/routing/locations.ts b/src/routing/locations.ts index aeb9f4f4f..c366b6f0e 100644 --- a/src/routing/locations.ts +++ b/src/routing/locations.ts @@ -47,6 +47,7 @@ export const locations = { itemEditor: (options?: ItemEditorParams) => injectParams('/item-editor', { itemId: 'item', collectionId: 'collection', isReviewing: 'reviewing', newItem: 'newItem' }, options), ens: () => '/names', + ensDetail: (name = ':name') => `/names/${name}`, worlds: () => '/worlds', curation: () => '/curation', templates: () => '/templates', diff --git a/src/specs/utils.tsx b/src/specs/utils.tsx new file mode 100644 index 000000000..0538dd15f --- /dev/null +++ b/src/specs/utils.tsx @@ -0,0 +1,47 @@ +import { Provider } from 'react-redux' +import flatten from 'flat' +import { render } from '@testing-library/react' +import { Store } from 'redux' +import { createMemoryHistory } from 'history' +import { en } from 'decentraland-dapps/dist/modules/translation/defaults' +import { mergeTranslations } from 'decentraland-dapps/dist/modules/translation/utils' +import TranslationProvider from 'decentraland-dapps/dist/providers/TranslationProvider' +import * as locales from 'modules/translation/languages' +import { RootState } from 'modules/common/types' +import { initTestStore } from 'modules/common/store' +import { ConnectedRouter } from 'connected-react-router' + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion +const allTranslations = mergeTranslations(flatten(en) as unknown as Record, flatten(locales.en)) +export function renderWithProviders( + component: JSX.Element, + { preloadedState, store }: { preloadedState?: Partial; store?: Store } = {} +) { + const initializedStore = + store || + initTestStore({ + ...(preloadedState || {}), + storage: { loading: false }, + translation: { + data: { + en: allTranslations, + 'en-EN': allTranslations + }, + locale: 'en-EN' + } + }) + + const history = createMemoryHistory() + + function AppProviders({ children }: { children: JSX.Element }) { + return ( + + + {children} + + + ) + } + + return render(component, { wrapper: AppProviders }) +} diff --git a/src/themes/components/CopyToClipboard.css b/src/themes/components/CopyToClipboard.css index 0bc74c1a0..e42d337cb 100644 --- a/src/themes/components/CopyToClipboard.css +++ b/src/themes/components/CopyToClipboard.css @@ -6,3 +6,20 @@ opacity: 0; transition: opacity 0.5s ease-out; } + +.copy-to-clipboard i { + background: #43404a; + width: 25px; + height: 25px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: white; + cursor: pointer; +} + +.copy-to-clipboard:hover i { + background: #555459; +}