Common modules for Decentraland dApps
npm install decentraland-dapps
Common modules for Decentraland dApps.
This version includes significant changes including React 18 upgrade and dependency management improvements following Decentraland's Dependency Management Standard.
This version requires React 18.0.0 or higher. React 17 is no longer supported.
#### What changed:
| Package | From | To |
| ------------------------ | ------- | ------- |
| react / react-dom | ^17.0.0 | ^18.0.0 |
| react-intl | ^5.20.0 | ^6.5.0 |
| react-redux | ^7.2.4 | ^8.1.3 |
| @types/react | ^17.0.0 | ^18.3.0 |
| @testing-library/react | ^12.1.0 | ^14.3.0 |
| decentraland-ui | ^6.0.0 | ^7.0.1 |
| decentraland-ui2 | ^0.44.0 | ^1.0.1 |
#### Migration notes:
- JSX Transform: Updated from react to react-jsx. You no longer need to import React in files that only use JSX.
- RootStateOrAny removed: RootStateOrAny was removed in react-redux v8. Use any or define your own RootState type.
- Node.js requirement: Now requires Node.js >=20.0.0 and npm >=10.0.0
The following packages are required and will be automatically installed by npm v7+ when you install decentraland-dapps. If your project already has these dependencies, npm will use your existing versions (as long as they satisfy the version ranges).
> Note: If you're using npm v6 or earlier, you'll need to install these manually.
| Package | Version | Notes |
| ---------------------- | -------------------- | ---------------------------- |
| react | ^18.0.0 | ⚠️ Updated from React 17 |
| react-redux | ^7.2.4 \|\| ^8.0.0 | |
| react-router | ^5.3.4 | |
| redux | ^4.1.0 | |
| redux-saga | ^1.1.3 | |
| history | ^4.10.1 | |
| @dcl/crypto | ^3.3.1 | Moved from dependencies |
| @dcl/schemas | ^19.8.0 | Moved from dependencies |
| @dcl/ui-env | ^1.5.0 | Moved from dependencies |
| decentraland-connect | ^9.1.0 | Moved from dependencies |
| decentraland-ui | ^7.0.0 | Moved from dependencies |
| decentraland-ui2 | ^1.0.0 | Moved from dependencies |
| ethers | ^5.7.2 | Moved from dependencies |
| react-intl | ^6.5.0 | Moved from dependencies |
> Note: These packages were moved to peerDependencies to ensure a single instance is shared across your application and prevent issues with React Context, singletons, and bundle size.
---
- Modules
- Wallet
- Storage
- Transaction
- Authorization
- Translation
- Analytics
- Loading
- Modal
- Toasts
- Profile
- Credits
- Lib
- API
- ETH
- Entities
- Containers
- App
- Navbar
- Footer
- SignInPage
- Modal
- TransactionLink
- Components
- Intercom
Common redux modules for dApps.
This module takes care of connecting to MetaMask/Ledger, and insert in the state some useful information like address, network, mana and derivationPath.
You can use the following selectors importing them from decentraland-dapps/dist/modules/wallet/selectors:
``tsx`
getData = (state: State) => BaseWallet
getError = (state: State) => string
getNetwork = (state: State) => 'mainnet' | 'ropsten' | 'rinkeby' | 'kovan' | 'localhost'
getAddress = (state: State) => string
isConnected = (state: State) => boolean
isConnecting = (state: State) => boolean
Also you can hook to the following actions from your reducers/sagas by importing them from decentraland-dapps/dist/modules/wallet/actions:
`tsx`
CONNECT_WALLET_REQUEST
CONNECT_WALLET_SUCCESS
CONNECT_WALLET_FAILURE
Also you can import types for those actions from that same file:
`tsx`
ConnectWalletRequestAction
ConnectWalletSuccessAction
ConnectWalletFailureAction
This is an example of how you can wait for the CONNECT_WALLET_SUCCESS action to trigger other actions:
`tsx
// modules/something/sagas.ts
import { CONNECT_WALLET_SUCCESS, ConnectWalletSuccessAction } from 'decentraland-dapps/dist/modules/wallet/actions'
import { fetchSomethingRequest } from './actions'
export function* saga() {
yield takeLatest(CONNECT_WALLET_SUCCESS, handleConnectWalletSuccess)
}
function* handleConnectWalletSuccess(action: ConnectWalletSuccessAction) {
yield put(fetchSomethingRequest())
}
`
In order to install this module you will need to add a provider, a reducer and a saga to your dapps.
Provider:
Add the as a child of your redux provider. If you use react-router-redux or connected-react-router make sure the is a child of the and not the other way around, like this:
`tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import WalletProvider from 'decentraland-dapps/dist/providers/WalletProvider'
import { store, history } from './store'
ReactDOM.render(
document.getElementById('root')
)
`
Reducer:
Import the walletReducer and add it at the root level of your dApp's reducer as wallet, like this:
`ts
import { combineReducers } from 'redux'
import { walletReducer as wallet } from 'decentraland-dapps/dist/modules/wallet/reducer'
export const rootReducer = combineReducers({
wallet
// your other reducers
})
`
Saga:
You will need to create a walletSaga and add it to your rootSaga:
`ts
import { all } from 'redux-saga/effects'
import { walletSaga } from 'decentraland-dapps/dist/modules/wallet/sagas'
export function* rootSaga() {
yield all([
walletSaga()
// your other sagas here
])
}
`
You'll need to supply in which chain you're going to work. It won't affect wallets like Metamask, where you can choose which network to use on the wallet itself, but's necessary for things like email/phone based wallets.
If you're using the Navbar container, this chain will determine in which chain the user must be. If they're on the incorrect chain (using a network picker with Metamask for example), a modal will pop up blocking the dapp until the state changes.
Remember that the chain id is the number that represents a particular network, 1 being mainnet, 3 being ropsten, etc.
Instead of importing walletSaga Saga: ` const walletSaga = createWalletSaga({ CHAIN_ID: process.env.chainId }) Actions: If you want to hook a callback to connect the wallet, there're two things to keep in mind. The process of connecting a wallet consists in two steps, first enabling ` With it's corresponding actions and types from the same file: ` EnableWalletRequestAction The wallet saga will listen for ENABLE_WALLET_SUCCESS All of this is handled by SignInPage behind the scenes, so you can just use that instead. Remember to add Learn More
, use createWalletSaga:ts
import { all } from 'redux-saga/effects'
import { createWalletSaga } from 'decentraland-dapps/dist/modules/wallet/sagas'
export function* rootSaga() {
yield all([
walletSaga()
// your other sagas here
])
}
` it and then properly connecting it. The set of actions to keep in mind are the following (all from decentraland-dapps/dist/modules/wallet/actions):tsx`
enableWalletRequest
enableWalletSuccess
enableWalletFailuretsx
ENABLE_WALLET_REQUEST
ENABLE_WALLET_SUCCESS
ENABLE_WALLET_FAILURE
EnableWalletSuccessAction
EnableWalletFailureAction
` and automatically call CONNECT_WALLET_REQUEST. If you use connect wallet without enabling first it will only work if you enabled first and it'll stop working once the user disconnects the wallet from the site (if she ever does).
The wallet module includes utilities to send transaction and meta transactions automatically. Meta transactions are sent when trying to send a transaction to a network different from the one connected to.
Web2 wallets will be prompted to accept or reject the transaction using the Web2TransactionModal. This modal is required for the sendTransaction function to work and must be initiated in your dApp.
The storage module allows you to save parts of the redux store in localStorage to make them persistent and migrate it from different versions without loosing it.
This module is required to use other modules like Transaction, Translation and Wallet.
You need to add a middleware and two reducers to your dApp.
Middleware:
You will need to create a storageMiddleware and add apply it along with your other middlewares:
`ts
// store.ts
import { applyMiddleware, compose, createStore } from 'redux'
import { createStorageMiddleware } from 'decentraland-dapps/dist/modules/storage/middleware'
import { migrations } from './migrations'
const composeEnhancers =
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const { storageMiddleware, loadStorageMiddleware } = createStorageMiddleware({
storageKey: 'storage-key' // this is the key used to save the state in localStorage (required)
paths: [] // array of paths from state to be persisted (optional)
actions: [] // array of actions types that will trigger a SAVE (optional)
migrations: migrations // migration object that will migrate your localstorage (optional)
})
const middleware = applyMiddleware(
// your other middlewares
storageMiddleware
)
const enhancer = composeEnhancers(middleware)
const store = createStore(rootReducer, enhancer)
loadStorageMiddleware(store)
`
Migrations:
migrations looks like
migrations.ts:
`ts`
export const migrations = {
2: migrateToVersion2(data),
3: migrateToVersion3(data)
}
Where every key represent a migration and every method should return the new localstorage data:
`ts`
function migrateToVersion2(data) {
return omit(data, 'translations')
}
You don't need to care about updating the version of the migration because it will be set automatically.
Reducer:
You will need to add storageReducer as storage to your rootReducer and then wrap the whole reducer with storageReducerWrapper
`ts
import { combineReducers } from 'redux'
import { storageReducer as storage, storageReducerWrapper } from 'decentraland-dapps/dist/modules/storage/reducer'
export const rootReducer = storageReducerWrapper(
combineReducers({
storage
// your other reducers
})
)
`
This module is necessary to use other modules like Transaction, Translation and Wallet, but you can also use it to make other parts of your dApp's state persistent
The first parameter of createStorageMiddleware The second parameter is an array of paths from the state that you want to be stored, ie: ` That will make state.invites The third parameter is an array of action types that will trigger a SAVE of the state in localStorage, ie: ` This parameter is optional and is and you don't have to configure it to use the TransactionLearn More
is the key used to store the state data in localStorage (required).ts`
const paths = [['invites'][('user', 'name')]] and state.user.name persistent. This parameter is optional and you don't have to configure it to use the Transaction and/or Translation modules.ts`
const actionTypes = [SEND_INVITE_SUCCESS] and/or Translation modules.
The transaction module allows you to watch for pending transactions and keep track of the transaction history.
This module requires you to install the Storage module in order to work.
When you have an action that creates a transaction and you want to watch it, you can do with buildTransactionPayload:
`ts
import { action } from 'typesafe-actions'
import { buildTransactionPayload } from 'decentraland-dapps/dist/modules/transaction/utils'
// Send Invite
export const SEND_INVITE_REQUEST = '[Request] Send Invite'
export const SEND_INVITE_SUCCESS = '[Success] Send Invite'
export const SEND_INVITE_FAILURE = '[Failure] Send Invite'
export const sendInvitesRequest = (address: string) =>
action(SEND_INVITE_REQUEST, {
address
})
export const sendInvitesSuccess = (txHash: string, address: string) =>
action(SEND_INVITE_SUCCESS, {
...buildTransactionPayload(txHash, {
address
}),
address
})
export const sendInvitesFailure = (address: string, errorMessage: string) =>
action(SEND_INVITE_FAILURE, {
address,
errorMessage
})
export type SendInvitesRequestAction = ReturnType
export type SendInvitesSuccessAction = ReturnType
export type SendInvitesFailureAction = ReturnType
`
Or buildTransactionWithReceiptPayload if you need the tx event logs
`ts`
export const sendInvitesSuccess = (txHash: string, address: string) =>
action(SEND_INVITE_SUCCESS, {
...buildTransactionWithReceiptPayload(txHash, {
address
}),
address
})
It will save the event logs inside { receipt: { logs: [] } } after the tx was confirmed
Then you can use the selectors getPendingTransactions and getTransactionHistory from decentraland-dapps/dist/modules/transaction/selectors to get the list of pending transactions and the transaction history.
You need to add a middleware, a reducer and a saga to use this module.
Middleware:
Create the transactionMiddleware and apply it
`ts
// store.ts
import { createTransactionMiddleware } from 'decentraland-dapps/dist/modules/transaction/middleware'
const transactionMiddleware = createTransactionMiddleware()
const middleware = applyMiddleware(
// your other middlewares
transactionMiddleware
)
`
Reducer:
Add transactionReducer as transaction to your rootReducer
`ts
import { combineReducers } from 'redux'
import { transactionReducer as transaction } from 'decentraland-dapps/dist/modules/transaction/reducer'
export const rootReducer = combineReducers({
transaction
// your other reducers
})
`
Saga:
Add transactionSaga to your rootSaga
`ts
import { all } from 'redux-saga/effects'
import { transactionSaga } from 'decentraland-dapps/dist/modules/transaction/sagas'
export function* rootSaga() {
yield all([
transactionSaga()
// your other sagas
])
}
`
You can make your reducers listen to confirmed transactions and update your state accordingly
Taking the example of the SEND_INVITE_SUCCESS ` export type InviteState = { export type InviteReducerAction = export const INITIAL_STATE: InviteState = { export function invitesReducer(Learn More
action type shown in the Usage section above, let's say we want to decrement the amount of available invites after the transaction is mined, we can do so by adding the FETCH_TRANSACTION_SUCCESS action type in our reducer:diff
// modules/invite/reducer
import { AnyAction } from 'redux'
import { loadingReducer } from 'decentraland-dapps/dist/modules/loading/reducer'
import {
FETCH_INVITES_REQUEST,
FETCH_INVITES_SUCCESS,
FETCH_INVITES_FAILURE,
FetchInvitesSuccessAction,
FetchInvitesFailureAction,
FetchInvitesRequestAction,
SEND_INVITE_SUCCESS
} from './actions'
import { FETCH_TRANSACTION_SUCCESS, FetchTransactionSuccessAction } from 'decentraland-dapps/dist/modules/transaction/actions';
loading: AnyAction[]
data: {
[address: string]: number
}
error: null | string
}
| FetchInvitesRequestAction
| FetchInvitesSuccessAction
| FetchInvitesFailureAction
| FetchTransactionSuccessAction
loading: [],
data: {},
error: null
}
state: InviteState = INITIAL_STATE,
action: InviteReducerAction
): InviteState {
switch (action.type) {
case FETCH_INVITES_REQUEST: {
return {
...state,
loading: loadingReducer(state.loading, action)
}
}
case FETCH_INVITES_SUCCESS: {
return {
loading: loadingReducer(state.loading, action),
data: {
...state.data,
[action.payload.address]: action.payload.amount
},
error: null
}
}
case FETCH_INVITES_FAILURE: {
return {
...state,
loading: loadingReducer(state.loading, action),
error: action.payload.errorMessage
}
}
case FETCH_TRANSACTION_SUCCESS: {
const { transaction } = action.payload
switch (transaction.actionType) {
case SEND_INVITE_SUCCESS: {
const { address } = (transaction as any).payload
return {
...state,
data: {
...state.data,
[address]: state.data[address] - 1
}
}
}
default:
return state
}
}
default: {
return state
}
}
}
`
This module allows you to grant/revoke approvals to a token. It works for both allowance and approval for all.
This module depends on the wallet and the transactions module
After installing the module, you'll need to initialize the authorizations you want to query using the following action:
`ts`
fetchAuthorizationsRequest(authorizations: Authorization[])
That action will query the blockchain for each authorization and update the state so you can check it later. You can hook to:
`ts`
FETCH_AUTHORIZATIONS_REQUEST
FETCH_AUTHORIZATIONS_SUCCESS
FETCH_AUTHORIZATIONS_FAILURE
Once you have this hooked up, you can either grant or revoke a token by using:
`ts`
grantTokenRequest(authorization: Authorization)
revokeTokenRequest(authorization: Authorization)
You can hook to the following actions:
`ts
GRANT_TOKEN_REQUEST
GRANT_TOKEN_SUCCESS
GRANT_TOKEN_FAILURE
REVOKE_TOKEN_REQUEST
REVOKE_TOKEN_SUCCESS
REVOKE_TOKEN_FAILURE
`
Keep in mind that each of these actions send a transaction, so if you wan't to check if they're done, check the action type of the FETCH_TRANSACTION_SUCCESS action. More info on the transactions module
Reducer
Add the authorizationReducer as authorization to your rootReducer:
`ts
import { combineReducers } from 'redux'
import { authorizationReducer as authorization } from 'decentraland-dapps/dist/modules/authorization/reducer'
export const rootReducer = combineReducers({
authorization
// your other reducers
})
`
Sagas
Add the authorizationSaga to the rootSaga:
`ts
import { all } from 'redux-saga/effects'
import { authorizationSaga } from 'decentraland-dapps/dist/modules/authorization/sagas'
export function* rootSaga() {
yield all([
authorizationSaga()
// your other sagas
])
}
`
This module allows you to do i18n.
This module has an optional dependency on Storage module to cache translations and boot the application faster. To learn more read the Advanced Usage section of this module.
Using the helper t() you can add translations to your dApp
`tsx
import * as React from 'react'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
export default class BuyButton extends React.PureComponent {
render() {
return
}
}
`
Then you just have to provide locale files like this:
_en.json_
`json`
{
"buy_page": {
"buy_button": "Buy"
}
}
_es.json_
`json`
{
"buy_page": {
"buy_button": "Comprar"
}
}
Yon can dispatch the changeLocale(locale: string) action from decentraland-dapps/dist/modules/translation/actions to change the language
You will need to add a provider, a reducer and a saga to use this module
Provider:
Add the as a child of your redux provider, passing the locales that you want to support. If you use react-router-redux or connected-react-router make sure the is a child of the and not the other way around, like this:
`tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import TranslationProvider from 'decentraland-dapps/dist/providers/TranslationProvider'
import { store, history } from './store'
ReactDOM.render(
document.getElementById('root')
)
`
Reducer:
Add the translationReducer as translation to your rootReducer:
`ts
import { combineReducers } from 'redux'
import { translationReducer as translation } from 'decentraland-dapps/dist/modules/translation/reducer'
export const rootReducer = combineReducers({
translation
// your other reducers
})
`
Saga:
Create a translationSaga and add it to your rootSaga. You need to provide an object containing all the translations, or a function that takes the locale and returns a Promise of the translations for that locale (you can use that to fetch the translations from a server instead of bundling them in the app). Here are examples for the two options:
1. Bundling the translations in the dApp:
_en.json_
`json`
{
"buy_page": {
"buy_button": "Buy"
}
}
_es.json_
`json`
{
"buy_page": {
"buy_button": "Comprar"
}
}
_translations.ts_
`ts`
const en = require('./en.json')
const es = require('./es.json')
export { en, es }
_sagas.ts_
`ts
import { all } from 'redux-saga/effects'
import { createTranslationSaga } from 'decentraland-dapps/dist/modules/translation/sagas'
import * as translations from './translations'
export const translationSaga = createTranslationSaga({
translations
})
export function* rootSaga() {
yield all([
translationSaga()
// your other sagas
])
}
`
2. Fetching translations from server
_sagas.ts_
`ts
import { all } from 'redux-saga/effects'
import { createTranslationSaga } from 'decentraland-dapps/dist/modules/translation/sagas'
import { api } from 'lib/api'
export const translationSaga = createTranslationSaga({
getTranslation: locale => api.fetchTranslations(locale)
})
export function* rootSaga() {
yield all([
translationSaga()
// your other sagas
])
}
`
Read the Advanced Usage section below to learn how to cache translations and make your application boot faster.
You can use the Storage module to cache translations (read 2. Fetching translations from server above).
After installing the Storage module you can persist the translations by adding 'translation' ` const { storageMiddleware, loadStorageMiddleware } = createStorageMiddleware({ This will store the translation module in localStorageLearn More
to your storage middleware paths:ts
// store.ts
storageKey: 'my-dapp-storage',
paths: ['translation']
})
`, so next time your application is started it will boot with all the translations populated before even fetching them from the server.
The analytics module let's integrate Segment into your dApp.
You need to have the Wallet module installed in order to send identify events.
This module will import the segment snippet into your dApp. Be aware that the middleware must be loaded before using segment methods.
To send track events, add an analytics.ts file and require it from your entry point, and use the add() helper to add actions that you want to track:
`ts
// analytics.ts
import { add } from 'decentraland-dapps/dist/modules/analytics/utils'
import { CREATE_VOTE_SUCCESS, CreateVoteSuccessAction } from 'modules/vote/actions'
add(CREATE_VOTE_SUCCESS, 'Vote', (action: CreateVoteSuccessAction) => ({
poll_id: action.payload.vote.poll_id,
option_id: action.payload.vote.option_id,
address: action.payload.wallet.address
}))
`
The first parameter is the action type that you want to track (required).
The second parameter is the event name for that action (it will show up with that name in Segment). If none provided the action type will be used as the event name.
The third parameter is a function that takes the action and returns the data that you want to associate with that event (it will be sent to Segment). If none is provided the whole action will be sent.
You need to apply a middleware and a saga to use this module
Middleware:
`ts
// store.ts
import { createAnalyticsMiddleware } from '@dapps/modules/analytics/middleware'
const analyticsMiddleware = createAnalyticsMiddleware('SEGMENT WRITE KEY')
const middleware = applyMiddleware(
// your other middlewares
analyticsMiddleware
)
const enhancer = composeEnhancers(middleware)
const store = createStore(rootReducer, enhancer)
`
Saga:
`ts
import { all } from 'redux-saga/effects'
import { analyticsSaga } from 'decentraland-dapps/dist/modules/analytics/sagas'
export function* rootSaga() {
yield all([
analyticsSaga()
// your other sagas
])
}
`
In order to track all page change you will need to use the analytics page function. There is already an exported hook you can use the will be triggered everytime a location change in the app
Note: It is important that this hook is triggered in any component inside the router provider.
`ts
import usePageTracking from 'decentraland-dapps/dist/hooks/usePageTracking'
function Routes() {
usePageTracking()
/// Route rendering
}
`
You can use the same redux action type to generate different Segment events if you pass a function as the second parameter instead of a string:
`ts`
add(AUTHORIZE_LAND_SUCCESS, action => (action.isAuthorized ? 'Authorize LAND' : 'Unauthorize LAND'))
The loading module is used to keep track of async actions in the state.
You can use the selectors isLoading(state) and isLoadingType(state, ACTION_TYPE) from decentraland-dapps/dist/modules/loading/selectors to know if a domain has pending actions or if a specific action is still pending
In order to use these selectors you need to use the loadingReducer within your domain reducers, here is an example:
`ts
import { loadingReducer, LoadingState } from 'decentraland-dapps/dist/modules/loading/reducer'
import {
FETCH_INVITES_REQUEST,
FETCH_INVITES_SUCCESS,
FETCH_INVITES_FAILURE,
FetchInvitesSuccessAction,
FetchInvitesFailureAction,
FetchInvitesRequestAction
} from './actions'
export type InviteState = {
loading: LoadingState
data: {
[address: string]: number
}
error: null | string
}
export const INITIAL_STATE: InviteState = {
loading: [],
data: {},
error: null
}
export type InviteReducerAction = FetchInvitesRequestAction | FetchInvitesSuccessAction | FetchInvitesFailureAction
export function invitesReducer(state: InviteState = INITIAL_STATE, action: InviteReducerAction): InviteState {
switch (action.type) {
case FETCH_INVITES_REQUEST: {
return {
...state,
loading: loadingReducer(state.loading, action)
}
}
case FETCH_INVITES_SUCCESS: {
return {
loading: loadingReducer(state.loading, action),
data: {
...state.data,
[action.payload.address]: action.payload.amount
},
error: null
}
}
case FETCH_INVITES_FAILURE: {
return {
...state,
loading: loadingReducer(state.loading, action),
error: action.payload.errorMessage
}
}
default: {
return state
}
}
}
`
Now we can for example use the selector isLoadingType(state.invite.loading, FETCH_INVITES_REQUEST) to know if that particular action is still pending, or isLoading(states.invite) to know if there's any pending action for that domain.
Also, all the pending actions are stored in an array in state.invite.loading so we can use that information in the UI if needed (i.e. disable a button)
Leverages redux state and provides actions to open and close each modal by name. It provides a few simple actions:
`ts`
openModal(name: string, metadata: any = null)
closeModal(name: string)
closeAllModals()
toggleModal(name: sgtring)
It also provides a selector to get the open modals:
``
getOpenModals(state): ModalState
In order to use this module you need to add a reducer and a provider.
Provider:
Add the as a parent of your routes. It takes an object of { {modalName: string]: React.Component } as a prop (components). It'll use it to render the appropiate modal when you call openModal(name: string)
`tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import ModalProvider from 'decentraland-dapps/dist/providers/ModalProvider'
import * as modals from 'components/Modals'
import { store, history } from './store'
ReactDOM.render(
document.getElementById('root')
)
`
Reducer:
Add the modalReducer as modal to your rootReducer:
`ts
import { combineReducers } from 'redux'
import { modalReducer as modal } from 'decentraland-dapps/dist/modules/modal/reducer'
export const rootReducer = combineReducers({
modal
// your other reducers
})
`
You can have add more strict typing to the actions:
Types: ` export ModalName = keyof typeof modals Actions: ` const { openModal, closeModal, toggleModal } = getModalActions export * from 'decentraland-dapps/dist/modules/modal/actions'Learn More
The modal actions allow for a generic type for the name. So say you want to type the name of your available modals, you can create a modal module in your dApp and add the following files:ts
// modules/types/actions.ts
import * as modals from 'components/Modals' // same import as the one use for
`ts
// modules/modal/actions.ts
import { getModalActions } from 'decentraland-dapps/dist/modules/modal/actions'
import { ModalName } from './types'
export { openModal, closeModal, toggleModal }
`
Leverages redux state and provides actions to show and hide toasts. It provides a few simple actions:
`ts`
showToast(toast: Omit
hideToast(id: number)
You can check the properties a toast has here. It extends the props already defined on decentraland-ui's toast
It also provides a selector to get the open toasts:
``
getToasts(state): Toast[]
In order to use this module you need to add a reducer, a provider and a saga.
Provider:
Add the as a parent of your routes. It takes an optional position param to set where you want the toasts to appear. It'll default to top left
`tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import ToastProvider from 'decentraland-dapps/dist/providers/ToastProvider'
import * as modals from 'components/Modals'
import { store, history } from './store'
ReactDOM.render(
document.getElementById('root')
)
`
Reducer:
Add the toastReducer as toast to your rootReducer:
`ts
import { combineReducers } from 'redux'
import { toastReducer as toast } from 'decentraland-dapps/dist/modules/toast/reducer'
export const rootReducer = combineReducers({
toast
// your other reducers
})
`
Saga:
You will need to create a toastSaga and add it to your rootSaga:
`ts
import { all } from 'redux-saga/effects'
import { toastSaga } from 'decentraland-dapps/dist/modules/wallet/sagas'
export function* rootSaga() {
yield all([
toastSaga()
// your other sagas here
])
}
`
Toasts themselves do not do any async action, but this is needed to render each toast properly, without overloading the redux state with unnecesary information.
Leverages the redux state and provides actions and selectors to work with profiles.
The module exposes the following actions:
The loadProfileRequest action will trigger a profile fetch through the profile sagas that will result, if successful, in the profile metadata being loaded. The success and failure actions of the request action are also included and will be used to signal a successful or a failing request.
The setProfileAvatarDescriptionRequest action will trigger a change in the first avatar of the user's profile, that will result in a new entity being deployed for that profile, with the description of the avatar changed for the one specified in the action. The success and failure actions of the request action are also included and will be used to signal a successful or a failing request.
The clearProfileError action will clear any profile request errors from the store.
To install the profile module, just import it and add it to the store by combining the existing reducers with the one provided in the profile module.
`ts
import { profileReducer as profile } from 'decentraland-dapps/dist/modules/profile/reducer'
export const createRootReducer = (history: History) =>
combineReducers({
profile,
otherReducer
})
export type RootState = ReturnType
`
This module helps manage credits in the Decentraland marketplace. It handles fetching credits and provides real-time updates through Server-Sent Events (SSE).
You can start and stop real-time credit updates using SSE:
`ts
import { startCreditsSSE, stopCreditsSSE } from 'decentraland-dapps/dist/modules/credits/actions'
// Start real-time credit updates when component mounts
dispatch(startCreditsSSE(address))
// Stop real-time updates when component unmounts
dispatch(stopCreditsSSE())
`
For backward compatibility, the following aliases are also available:
`ts`
import {
startCreditsAutoPolling, // alias for startCreditsSSE
stopCreditsAutoPolling // alias for stopCreditsSSE
} from 'decentraland-dapps/dist/modules/credits/actions'
The module will automatically:
1. Fetch the initial credits state
2. Establish an SSE connection with the server
3. Update the credits in real-time whenever changes occur on the server
4. Check if the credits feature is enabled before establishing a connection
Selectors:
`ts
import { getCredits } from 'decentraland-dapps/dist/modules/credits/selectors'
const credits = getCredits(state, address)
`
Installation:
Add the creditsReducer to your root reducer:
`ts
import { combineReducers } from 'redux'
import { creditsReducer as credits } from 'decentraland-dapps/dist/modules/credits/reducer'
export const rootReducer = combineReducers({
credits
// your other reducers
})
`
Add the creditsSaga to your root saga:
`ts
import { all } from 'redux-saga/effects'
import { creditsSaga } from 'decentraland-dapps/dist/modules/credits/sagas'
import { CreditsClient } from 'decentraland-dapps/dist/modules/credits/CreditsClient'
// Create a credits client - make sure your server supports SSE at /users/{address}/credits/stream
const creditsClient = new CreditsClient(API_URL)
export function* rootSaga() {
yield all([
creditsSaga({ creditsClient })
// your other sagas
])
}
`
Server Requirements:
Your server needs to provide an SSE endpoint at /users/{address}/credits/stream that:
1. Keeps the connection open
2. Sends credit updates in the same format as the regular credits endpoint
3. Properly handles connection errors and retries
Here's an example of how the server might implement the SSE endpoint:
`typescript
// Server-side (Node.js with Express)
app.get('/users/:address/credits/stream', (req, res) => {
const { address } = req.params
// Set up SSE connection
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
// Send initial credits data
sendCreditsUpdate(address, res)
// Set up listener for credit changes for this address
const listener = (updatedAddress, creditsData) => {
if (updatedAddress === address) {
res.write(data: ${JSON.stringify(creditsData)}\n\n)
}
}
// Add listener to your event system
creditEventEmitter.on('credits-updated', listener)
// Clean up when connection closes
req.on('close', () => {
creditEventEmitter.off('credits-updated', listener)
})
})
`
Common libraries for dApps
The BaseAPI class can be extended to make requests and it handles the unwrapping of responses by decentraland-server
`ts
// lib/api
import { BaseAPI } from 'decentraland-dapps/dist/lib/api'
const URL = 'http://localhost/api'
export class API extends BaseAPI {
fetchSomething() {
return this.request('get', '/something', {})
}
}
export const api = new API(URL)
`
Ethereum helpers
Get user's connected provider without being wrapped by any library
`ts
import { getConnectedProvider } from 'decentraland-dapps/dist/lib/eth'
async function wrapProviderToEthers() {
const provider = await getConnectedProvider()
if (provider) {
return new etheres.providers.Web3Provider(provider)
}
}
`
Get an Eth instance with your lib of choice
`ts
import { Eth } from 'web3x/eth'
import { getConnectedProvider } from 'decentraland-dapps/dist/lib/eth'
async function doSomething() {
const provider = await getConnectedProvider()
if (!provider) throw new Error()
// web3x
const eth = new Eth(provider) // or new Eth(new LegacyProviderAdapter(provider))
// ethers
const eth = new ethers.providers.Web3Provider(provider)
}
`
- isCucumberProvider: Check if the provider is a cucumberProvider.isCoinbaseProvider
- : Check if the provider is a coinbaseProvider.isDapperProvider
- : Check if the provider is a _dapper's_ provider.isValidChainId
- : Check if the chain id is valid.
The entities library provides a set of methods to retrieve or deploy entities.
The deployEntity method does everything needed to deploy an entity that doesn't have new files. It pre-procceses the entity to prepare it for the deployment, it creates the auth chain and asks the user to sign the deployment of the entity and then deploys it.
`ts
// lib/entities
import { EntitesOperator } from 'decentraland-dapps/dist/lib/entities'
const URL = 'http://localhost/api'
const profileEntity = { ... }
const entitiesOperator = new EntitesOperator(URL)
await entitiesOperator.deployEntityWithoutNewFiles(
entity,
EntityTypes.PROFILE,
anAddress
)
`
The getProfileEntity gets the first profile of all the profiles an address has.
`ts
// lib/entities
import { EntitesOperator } from 'decentraland-dapps/dist/lib/entities'
const URL = 'http://localhost/api'
const entitiesOperator = new EntitesOperator(URL)
await entitiesOperator.getProfile(anAddress)
`
Common containers for dApps
The container can be used in the same way as the component from decentraland-ui but it's already connected to the redux store. You can override any NavbarProp if you want to connect differently, and you can pass all the regular NavbarProps to it.
This container requires you to install the Wallet. It also has support for i18n out of the box if you include the Translation module.
This is an example of a SomePage component that uses the container:
`tsx
import * as React from 'react'
import { Container } from 'decentraland-ui/dist/components/Container/Container'
import { NavbarPages } from 'decentraland-ui/dist/components/Navbar/Navbar.types'
import Navbar from 'decentraland-dapps/dist/containers/Navbar'
import './SomePage.css'
export default class SomePage extends React.PureComponent {
static defaultProps = {
children: null
}
render() {
const { children } = this.props
return (
<>
This
will show the user's blockie and mana balance because it is connected to the store.$3
If you are using the Translation module, the
Navbar contatiner comes with support for the 6 languages supported by the library.$3
You can override any of the default translations for any locale if you need to
Learn More
Say you want to override some translations in English, just include any or all of the following translations in your
en.json locale file:`json
{
"@dapps": {
"navbar": {
"account": {
"connecting": "Connecting...",
"signIn": "Sign In"
},
"menu": {
"agora": "Agora",
"blog": "Blog",
"docs": "Docs",
"marketplace": "Marketplace"
}
}
}
}
`
Footer
The