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 ? (
+
+ ) : (
+
+ )}
+ {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')}
+
+
+ {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 (
+
+
+
+
+
+
+
+
+
+ {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.name}.dcl.eth
+
+
+
+
+
+
+
+ {this.isAlias(ens) ? (
+
+ {this.props.avatar ? (
+
+ ) : (
+
+ )}
+ {ens.name}
+ {t('ens_list_page.table.you')}
+
+ ) : (
+
+ )}
+
+
+ {ens.ensAddressRecord ? (
+
+
+ {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')}
+
+
+ {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;
+}