diff --git a/webapp/config-overrides.js b/webapp/config-overrides.js index 764e7ea1d..f471c90ad 100644 --- a/webapp/config-overrides.js +++ b/webapp/config-overrides.js @@ -6,6 +6,7 @@ module.exports = override( path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules/react-virtualized-auto-sizer'), path.resolve(__dirname, 'node_modules/decentraland-connect/node_modules/@walletconnect'), - path.resolve(__dirname, 'node_modules/@walletconnect') + path.resolve(__dirname, 'node_modules/@walletconnect'), + path.resolve(__dirname, 'node_modules/@dcl/single-sign-on-client'), ]) ) diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 346fb52a6..1e72fca99 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@dcl/crypto": "^3.0.0", "@dcl/schemas": "^8.1.0", + "@dcl/single-sign-on-client": "^0.0.11", "@dcl/ui-env": "^1.2.0", "@ethersproject/providers": "^5.6.2", "@well-known-components/fetch-component": "^2.0.1", @@ -1989,59 +1990,15 @@ "integrity": "sha512-lfLtj4e646xCl/09seLq8zVnbuQ2w1O0c678TRB+1dttGYPpD13osX3J1P7ikC2SoR4NEBgDlFYXGbppmf0NKQ==" }, "node_modules/@dcl/crypto": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@dcl/crypto/-/crypto-3.4.2.tgz", - "integrity": "sha512-yUZeWrOVZNnPBiYhDd3M6z/QJWKEHvAFPaFhGmq2vtzJ3myIzH3dpGdJ4YKpkyhBddOaHhwfa9MUIv+30ve+Ew==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@dcl/crypto/-/crypto-3.4.3.tgz", + "integrity": "sha512-biG8PqImY3jtVM5bPizgAD0P6X9/7VMxKuD2hxiEEeZClfB1Zi76Qk8pHaLpJ5pU3B+NogwZ8xoREhq0Po+gXQ==", "dependencies": { - "@dcl/schemas": "^7.3.4", + "@dcl/schemas": "^8.2.0", "eth-connect": "^6.0.3", "ethereum-cryptography": "^1.0.3" } }, - "node_modules/@dcl/crypto/node_modules/@dcl/schemas": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-7.6.0.tgz", - "integrity": "sha512-TDIk7tDc4VB9ky0EgVUjFB/EBKkgjadcQ/ia4b9kU5vYlx7XUSB2kao/EDNRFfR17W1pssmZlUDtjmzFWTYSgQ==", - "dependencies": { - "ajv": "^8.11.0", - "ajv-errors": "^3.0.0", - "ajv-keywords": "^5.1.0" - } - }, - "node_modules/@dcl/crypto/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@dcl/crypto/node_modules/ajv-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", - "peerDependencies": { - "ajv": "^8.0.1" - } - }, - "node_modules/@dcl/crypto/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/@dcl/crypto/node_modules/ethereum-cryptography": { "version": "1.0.3", "license": "MIT", @@ -2138,9 +2095,9 @@ } }, "node_modules/@dcl/schemas": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-8.1.0.tgz", - "integrity": "sha512-1A7st/fESASmss+T1Vxc9lo3LHqSvgfDTLtLD2uhdgtOS0RombNES3KG/2zDfBpg9v3DCEchN6i7mWiCEw7/6g==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-8.2.2.tgz", + "integrity": "sha512-IZqcT1YOKxw5XWs6LW6Uw+7Ue5vHCVERPMwefAdt26jW1OTH818od0rBc1tQzzfBTwsrAvbgFJvpbZedieu00g==", "dependencies": { "ajv": "^8.11.0", "ajv-errors": "^3.0.0", @@ -2181,6 +2138,14 @@ "ajv": "^8.8.2" } }, + "node_modules/@dcl/single-sign-on-client": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@dcl/single-sign-on-client/-/single-sign-on-client-0.0.11.tgz", + "integrity": "sha512-/8YpgpiflBCB7NJWmW71+Ik5D3rzaMvxirT5NmIWTLY/p1XuGseyzlSrd0nlis1Xhn7xxcIegAWIoflDeZJ7Lw==", + "dependencies": { + "@dcl/crypto": "^3.4.3" + } + }, "node_modules/@dcl/ui-env": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@dcl/ui-env/-/ui-env-1.2.0.tgz", @@ -29273,50 +29238,15 @@ "integrity": "sha512-lfLtj4e646xCl/09seLq8zVnbuQ2w1O0c678TRB+1dttGYPpD13osX3J1P7ikC2SoR4NEBgDlFYXGbppmf0NKQ==" }, "@dcl/crypto": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@dcl/crypto/-/crypto-3.4.2.tgz", - "integrity": "sha512-yUZeWrOVZNnPBiYhDd3M6z/QJWKEHvAFPaFhGmq2vtzJ3myIzH3dpGdJ4YKpkyhBddOaHhwfa9MUIv+30ve+Ew==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@dcl/crypto/-/crypto-3.4.3.tgz", + "integrity": "sha512-biG8PqImY3jtVM5bPizgAD0P6X9/7VMxKuD2hxiEEeZClfB1Zi76Qk8pHaLpJ5pU3B+NogwZ8xoREhq0Po+gXQ==", "requires": { - "@dcl/schemas": "^7.3.4", + "@dcl/schemas": "^8.2.0", "eth-connect": "^6.0.3", "ethereum-cryptography": "^1.0.3" }, "dependencies": { - "@dcl/schemas": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-7.6.0.tgz", - "integrity": "sha512-TDIk7tDc4VB9ky0EgVUjFB/EBKkgjadcQ/ia4b9kU5vYlx7XUSB2kao/EDNRFfR17W1pssmZlUDtjmzFWTYSgQ==", - "requires": { - "ajv": "^8.11.0", - "ajv-errors": "^3.0.0", - "ajv-keywords": "^5.1.0" - } - }, - "ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", - "requires": {} - }, - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, "ethereum-cryptography": { "version": "1.0.3", "requires": { @@ -29386,9 +29316,9 @@ } }, "@dcl/schemas": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-8.1.0.tgz", - "integrity": "sha512-1A7st/fESASmss+T1Vxc9lo3LHqSvgfDTLtLD2uhdgtOS0RombNES3KG/2zDfBpg9v3DCEchN6i7mWiCEw7/6g==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@dcl/schemas/-/schemas-8.2.2.tgz", + "integrity": "sha512-IZqcT1YOKxw5XWs6LW6Uw+7Ue5vHCVERPMwefAdt26jW1OTH818od0rBc1tQzzfBTwsrAvbgFJvpbZedieu00g==", "requires": { "ajv": "^8.11.0", "ajv-errors": "^3.0.0", @@ -29422,6 +29352,14 @@ } } }, + "@dcl/single-sign-on-client": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@dcl/single-sign-on-client/-/single-sign-on-client-0.0.11.tgz", + "integrity": "sha512-/8YpgpiflBCB7NJWmW71+Ik5D3rzaMvxirT5NmIWTLY/p1XuGseyzlSrd0nlis1Xhn7xxcIegAWIoflDeZJ7Lw==", + "requires": { + "@dcl/crypto": "^3.4.3" + } + }, "@dcl/ui-env": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@dcl/ui-env/-/ui-env-1.2.0.tgz", diff --git a/webapp/package.json b/webapp/package.json index 0e3fc2633..e687a79da 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -4,6 +4,7 @@ "dependencies": { "@dcl/crypto": "^3.0.0", "@dcl/schemas": "^8.1.0", + "@dcl/single-sign-on-client": "^0.0.11", "@dcl/ui-env": "^1.2.0", "@ethersproject/providers": "^5.6.2", "@well-known-components/fetch-component": "^2.0.1", @@ -113,5 +114,10 @@ "last 1 safari version" ] }, - "homepage": "" + "homepage": "", + "jest": { + "moduleNameMapper": { + "@dcl/single-sign-on-client": "identity-obj-proxy" + } + } } diff --git a/webapp/src/config/env/dev.json b/webapp/src/config/env/dev.json index 4500e8ea4..a8c10a188 100644 --- a/webapp/src/config/env/dev.json +++ b/webapp/src/config/env/dev.json @@ -37,5 +37,6 @@ "MIN_SALE_VALUE_IN_WEI": "1000000000000000000", "EXPLORER_URL": "https://play.decentraland.zone", "MARKETPLACE_URL": "https://market.decentraland.zone", - "PROFILE_URL": "https://profile.decentraland.zone" + "PROFILE_URL": "https://profile.decentraland.zone", + "SSO_URL": "https://id.decentraland.zone" } diff --git a/webapp/src/config/env/prod.json b/webapp/src/config/env/prod.json index d17017df1..121a4e841 100644 --- a/webapp/src/config/env/prod.json +++ b/webapp/src/config/env/prod.json @@ -37,5 +37,6 @@ "MIN_SALE_VALUE_IN_WEI": "1000000000000000000", "EXPLORER_URL": "https://play.decentraland.org", "MARKETPLACE_URL": "https://market.decentraland.org", - "PROFILE_URL": "https://profile.decentraland.org" + "PROFILE_URL": "https://profile.decentraland.org", + "SSO_URL": "https://id.decentraland.org" } diff --git a/webapp/src/config/env/stg.json b/webapp/src/config/env/stg.json index dddcc817c..400df8196 100644 --- a/webapp/src/config/env/stg.json +++ b/webapp/src/config/env/stg.json @@ -37,5 +37,6 @@ "MIN_SALE_VALUE_IN_WEI": "1000000000000000000", "EXPLORER_URL": "https://play.decentraland.org", "MARKETPLACE_URL": "https://market.decentraland.today", - "PROFILE_URL": "https://profile.decentraland.today" + "PROFILE_URL": "https://profile.decentraland.today", + "SSO_URL": "https://id.decentraland.today" } diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index bc6c93f39..44eb16774 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -1,7 +1,7 @@ import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import { ConnectedRouter } from 'connected-react-router' -import { ScrollToTop } from './components/ScrollToTop' +import * as SingleSignOn from '@dcl/single-sign-on-client' import WalletProvider from 'decentraland-dapps/dist/providers/WalletProvider' import ToastProvider from 'decentraland-dapps/dist/providers/ToastProvider' import TranslationProvider from 'decentraland-dapps/dist/providers/TranslationProvider' @@ -11,14 +11,21 @@ import './setup' import './modules/analytics/track' import './modules/analytics/rollbar' +import { ScrollToTop } from './components/ScrollToTop' import * as locales from './modules/translation/locales' import { initStore, history } from './modules/store' import { Routes } from './components/Routes' import * as modals from './components/Modals' +import { config } from './config' import './themes' import './index.css' +// Initializes the SSO client. +// This will create a new iframe and append it to the body. +// It is ideal to do this as soon as possible to avoid any availability issues. +SingleSignOn.init(config.get('SSO_URL')) + async function main() { const component = ( diff --git a/webapp/src/modules/identity/sagas.spec.ts b/webapp/src/modules/identity/sagas.spec.ts index 33caa8964..130d9b1a2 100644 --- a/webapp/src/modules/identity/sagas.spec.ts +++ b/webapp/src/modules/identity/sagas.spec.ts @@ -1,14 +1,30 @@ import { expectSaga } from 'redux-saga-test-plan' -import { put } from 'redux-saga-test-plan/matchers' -import { select } from 'redux-saga/effects' +import { call } from 'redux-saga/effects' import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types' -import { connectWalletSuccess } from 'decentraland-dapps/dist/modules/wallet/actions' -import { getCurrentIdentity } from './selectors' -import { identitySaga } from './sagas' -import { generateIdentityRequest } from './actions' +import { + connectWalletSuccess, + disconnectWallet +} from 'decentraland-dapps/dist/modules/wallet/actions' +import { getIdentity, clearIdentity } from '@dcl/single-sign-on-client' +import { identitySaga, setAuxAddress } from './sagas' +import { generateIdentityRequest, generateIdentitySuccess } from './actions' + +jest.mock('@dcl/single-sign-on-client', () => { + return { + getIdentity: jest.fn(), + clearIdentity: jest.fn() + } +}) + +beforeEach(() => { + jest.resetAllMocks() + + setAuxAddress(null) +}) describe('when handling the wallet connection success', () => { let wallet: Wallet + beforeEach(() => { wallet = { address: '0x0' @@ -18,10 +34,7 @@ describe('when handling the wallet connection success', () => { describe("and there's no identity", () => { it('should put an action to generate the identity', () => { return expectSaga(identitySaga) - .provide([ - [select(getCurrentIdentity), null], - [put(generateIdentityRequest(wallet.address)), undefined] - ]) + .provide([[call(getIdentity, wallet.address), null]]) .put(generateIdentityRequest(wallet.address)) .dispatch(connectWalletSuccess(wallet)) .run({ silenceTimeout: true }) @@ -29,12 +42,46 @@ describe('when handling the wallet connection success', () => { }) describe("and there's an identity", () => { - it('should not put an action to generate the identity', () => { + it('should put an action to store the identity', () => { + const identity = {} as any + return expectSaga(identitySaga) - .provide([[select(getCurrentIdentity), {}]]) - .not.put(generateIdentityRequest(wallet.address)) + .provide([[call(getIdentity, wallet.address), identity]]) + .put(generateIdentitySuccess(wallet.address, identity)) .dispatch(connectWalletSuccess(wallet)) .run({ silenceTimeout: true }) }) }) }) + +describe('when handling the disconnect', () => { + describe('when the auxiliary address is set', () => { + const address = '0xSomeAddress' + + beforeEach(() => { + setAuxAddress(address) + }) + + it('should call the sso client to clear the identity', async () => { + await expectSaga(identitySaga) + .dispatch(disconnectWallet()) + .run({ silenceTimeout: true }) + + expect(clearIdentity).toHaveBeenCalledWith(address) + }) + }) + + describe('when the auxiliary address is not set', () => { + beforeEach(() => { + setAuxAddress(null) + }) + + it('should not call the sso client to clear the identity', async () => { + await expectSaga(identitySaga) + .dispatch(disconnectWallet()) + .run({ silenceTimeout: true }) + + expect(clearIdentity).not.toHaveBeenCalled() + }) + }) +}) diff --git a/webapp/src/modules/identity/sagas.ts b/webapp/src/modules/identity/sagas.ts index 8f19f7659..1b8cdecac 100644 --- a/webapp/src/modules/identity/sagas.ts +++ b/webapp/src/modules/identity/sagas.ts @@ -1,8 +1,17 @@ -import { takeLatest, put, call, select } from 'redux-saga/effects' +import { takeLatest, put, call } from 'redux-saga/effects' import { ethers } from 'ethers' import { Authenticator, AuthIdentity } from '@dcl/crypto' +import { + getIdentity, + storeIdentity, + clearIdentity +} from '@dcl/single-sign-on-client' import { t } from 'decentraland-dapps/dist/modules/translation/utils' -import { CONNECT_WALLET_SUCCESS } from 'decentraland-dapps/dist/modules/wallet/actions' +import { + CONNECT_WALLET_SUCCESS, + DISCONNECT_WALLET, + DisconnectWalletAction +} from 'decentraland-dapps/dist/modules/wallet/actions' import { ConnectWalletSuccessAction } from 'decentraland-dapps/dist/modules/wallet/actions' import { isErrorWithMessage } from '../../lib/error' import { getEth } from '../wallet/utils' @@ -15,11 +24,11 @@ import { generateIdentitySuccess } from './actions' import { IDENTITY_EXPIRATION_IN_MINUTES } from './utils' -import { getCurrentIdentity } from './selectors' export function* identitySaga() { yield takeLatest(GENERATE_IDENTITY_REQUEST, handleGenerateIdentityRequest) yield takeLatest(CONNECT_WALLET_SUCCESS, handleConnectWalletSuccess) + yield takeLatest(DISCONNECT_WALLET, handleDisconnect) } function* handleGenerateIdentityRequest(action: GenerateIdentityRequestAction) { @@ -44,6 +53,9 @@ function* handleGenerateIdentityRequest(action: GenerateIdentityRequestAction) { message => signer.signMessage(message) ) + // Stores the identity into the SSO iframe. + yield call(storeIdentity, address, identity) + yield put(generateIdentitySuccess(address, identity)) } catch (error) { yield put( @@ -55,10 +67,34 @@ function* handleGenerateIdentityRequest(action: GenerateIdentityRequestAction) { } } +// Persist the address of the connected wallet. +// This is a workaround for when the user disconnects as there is no selector that provides the address at that point +let auxAddress: string | null = null + +export function setAuxAddress(address: string | null) { + auxAddress = address +} + function* handleConnectWalletSuccess(action: ConnectWalletSuccessAction) { - const identity: AuthIdentity = yield select(getCurrentIdentity) + const address = action.payload.wallet.address + + yield call(setAuxAddress, address) + + // Obtains the identity from the SSO iframe. + const identity: AuthIdentity | null = yield call(getIdentity, address) + + // If the identity was persisted in the iframe, store in in redux. + // If not, generate a new one, which wil be stored in the iframe. if (!identity) { - // Generate a new identity - yield put(generateIdentityRequest(action.payload.wallet.address)) + yield put(generateIdentityRequest(address)) + } else { + yield put(generateIdentitySuccess(address, identity)) + } +} + +function* handleDisconnect(_action: DisconnectWalletAction) { + if (auxAddress) { + // Clears the identity from the SSO iframe when the user disconnects the wallet. + yield call(clearIdentity, auxAddress) } } diff --git a/webapp/src/modules/store.ts b/webapp/src/modules/store.ts index 8c6302105..a3104c4bd 100644 --- a/webapp/src/modules/store.ts +++ b/webapp/src/modules/store.ts @@ -14,7 +14,6 @@ import { createRootReducer, RootState } from './reducer' import { rootSaga } from './sagas' import { fetchTilesRequest } from './tile/actions' import { ARCHIVE_BID, UNARCHIVE_BID } from './bid/actions' -import { GENERATE_IDENTITY_SUCCESS } from './identity/actions' import { SET_IS_TRYING_ON } from './ui/preview/actions' import { getCurrentIdentity } from './identity/selectors' import { AuthIdentity } from 'decentraland-crypto-fetch' @@ -49,13 +48,11 @@ export function initStore() { paths: [ ['ui', 'archivedBidIds'], ['ui', 'preview', 'isTryingOn'], - ['identity', 'data'] ], // array of paths from state to be persisted (optional) actions: [ CLEAR_TRANSACTIONS, ARCHIVE_BID, UNARCHIVE_BID, - GENERATE_IDENTITY_SUCCESS, SET_IS_TRYING_ON ], // array of actions types that will trigger a SAVE (optional) migrations: {} // migration object that will migrate your localstorage (optional) @@ -105,13 +102,11 @@ export function initTestStore(preloadedState = {}) { paths: [ ['ui', 'archivedBidIds'], ['ui', 'preview', 'isTryingOn'], - ['identity', 'data'] ], // array of paths from state to be persisted (optional) actions: [ CLEAR_TRANSACTIONS, ARCHIVE_BID, UNARCHIVE_BID, - GENERATE_IDENTITY_SUCCESS, SET_IS_TRYING_ON ], // array of actions types that will trigger a SAVE (optional) migrations: {} // migration object that will migrate your localstorage (optional)