Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Commit

Permalink
Merge branch 'dev' into components-support-eip3770-prefixes
Browse files Browse the repository at this point in the history
  • Loading branch information
DiogoSoaress committed Nov 8, 2021
2 parents 8261959 + 74fda39 commit 16cc7cd
Show file tree
Hide file tree
Showing 37 changed files with 568 additions and 1,065 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ on:

env:
REPO_NAME_ALPHANUMERIC: safereact
STAGING_BUCKET_NAME: ${{ secrets.STAGING_MAINNET_BUCKET_NAME }}
STAGING_BUCKET_NAME: ${{ secrets.STAGING_BUCKET_NAME }}
REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN_MAINNET }}
REACT_APP_GOOGLE_ANALYTICS: ${{ secrets.REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET }}
REACT_APP_GNOSIS_APPS_URL: ${{ secrets.REACT_APP_GNOSIS_APPS_URL_PROD }}
Expand Down Expand Up @@ -74,7 +74,12 @@ jobs:
run: echo "REACT_APP_ENV=dev" >> $GITHUB_ENV
if: github.ref != 'refs/heads/main'

# Set production flag
# Set production flag on staging
- name: Set production flag for staging
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
if: github.ref == 'refs/heads/main'

# Set production flag on prod
- name: Set production flag for release PR or tagged build
run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'main'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ src/types/gateway/
tsconfig.tsbuildinfo
public/**/*.js
jest.results.json
*.#*
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,6 @@ When pushing to the `main` branch, the code will be automatically deployed to [s

Deployment to production is done manually. Please see the [release procedure](docs/release-procedure.md) notes for details.

## Configuring the app for running on different networks

[Please check the network configuration documentation](./docs/networks.md)

## Built With

- [React](https://reactjs.org/) - A JS library for building user interfaces
Expand Down
21 changes: 14 additions & 7 deletions docs/release-procedure.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
We prepare at least one release every sprint. Sprints are two weeks long.

### Prepare a branch
* A code-freeze branch named `release/X.Y.Z` is created
* A commit that bumps the version in the `package.json` is made
* Create a code-freeze branch named `release/X.Y.Z`
* Bump the version in the `package.json`
* Create a PR with the list of changes

💡 To generate a quick changelog:
```
git log origin/main..origin/dev --pretty=format:'* %s'
```

### QA
* The QA team do regression testing on this branch
Expand All @@ -13,11 +19,12 @@ We prepare at least one release every sprint. Sprints are two weeks long.
* `main` is automatically deployed to staging – some extra QA can be done there if needed

### Tag & release
* A version tag must be created and pushed.
* Create and push a new version tag :
```
git tag v3.7.0
git tag v3.15.0
git push --tags
```
* Devops are notified on Slack to deploy the tag to production
* A [GitHub release](https://github.com/gnosis/safe-react/releases) is created
* `main` is back-merged into the `dev` branch

* Create a [GitHub release](https://github.com/gnosis/safe-react/releases) for this tag
* Notify devops on Slack and send them the release link to deploy to production
* Back-merge `main` into the `dev` branch to keep them in sync
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
"dependencies": {
"@gnosis.pm/safe-apps-sdk": "4.3.0-next.2",
"@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2",
"@gnosis.pm/safe-core-sdk": "^0.3.1",
"@gnosis.pm/safe-core-sdk": "^1.0.0",
"@gnosis.pm/safe-deployments": "^1.2.0",
"@gnosis.pm/safe-react-components": "^0.8.5",
"@gnosis.pm/safe-react-gateway-sdk": "^2.4.0",
Expand All @@ -181,7 +181,7 @@
"abi-decoder": "^2.4.0",
"axios": "0.21.4",
"bignumber.js": "9.0.1",
"bnc-onboard": "~1.34.0",
"bnc-onboard": "~1.35.3",
"classnames": "^2.2.6",
"currency-flags": "3.2.1",
"date-fns": "^2.20.2",
Expand Down
27 changes: 24 additions & 3 deletions src/components/ExecuteCheckbox/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import { fireEvent, render, screen } from 'src/utils/test-utils'
import { fireEvent, render, screen, waitFor, act } from 'src/utils/test-utils'
import { history } from 'src/routes/routes'
import ExecuteCheckbox from '.'

jest.mock('src/logic/safe/store/actions/utils', () => {
const originalModule = jest.requireActual('src/logic/safe/store/actions/utils')

return {
__esModule: true, // Use it when dealing with esModules
...originalModule,
getLastTx: jest.fn(() => Promise.resolve({ isExecuted: true })),
}
})

describe('ExecuteCheckbox', () => {
it('should call onChange when checked/unchecked', () => {
it('should call onChange when checked/unchecked', async () => {
const onChange = jest.fn()
render(<ExecuteCheckbox onChange={onChange} />)
history.push('/rin:0xb3b83bf204C458B461de9B0CD2739DB152b4fa5A/balances')

await act(async () => {
render(<ExecuteCheckbox onChange={onChange} />)
})

await waitFor(() => {
expect(screen.getByTestId('execute-checkbox')).toBeInTheDocument()
})

fireEvent.click(screen.getByTestId('execute-checkbox'))
expect(onChange).toHaveBeenCalledWith(false)

fireEvent.click(screen.getByTestId('execute-checkbox'))
expect(onChange).toHaveBeenCalledWith(true)
})
Expand Down
25 changes: 20 additions & 5 deletions src/components/ExecuteCheckbox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
import { ReactElement } from 'react'
import { ReactElement, useEffect, useState } from 'react'
import { Checkbox, FormControlLabel } from '@material-ui/core'
import { getLastTx } from 'src/logic/safe/store/actions/utils'
import Row from 'src/components/layout/Row'
import { extractSafeAddress } from 'src/routes/routes'

interface ExecuteCheckboxProps {
onChange: (val: boolean) => unknown
}

const ExecuteCheckbox = ({ onChange }: ExecuteCheckboxProps): ReactElement => {
const ExecuteCheckbox = ({ onChange }: ExecuteCheckboxProps): ReactElement | null => {
const [isVisible, setVisible] = useState<boolean>(false)
const safeAddress = extractSafeAddress()

const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
onChange(e.target.checked)
}

return (
useEffect(() => {
if (!safeAddress) return

const checkLastTx = async () => {
const lastTx = await getLastTx(safeAddress)
setVisible(!lastTx || lastTx.isExecuted)
}
checkLastTx()
}, [safeAddress, setVisible])

return isVisible ? (
<Row margin="md">
<FormControlLabel
control={<Checkbox defaultChecked={true} color="primary" onChange={handleChange} />}
control={<Checkbox defaultChecked color="primary" onChange={handleChange} />}
label="Execute transaction"
data-testid="execute-checkbox"
/>
</Row>
)
) : null
}

export default ExecuteCheckbox
6 changes: 3 additions & 3 deletions src/components/GlobalErrorBoundary/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ describe('handleChunkError', () => {
it('handles no sessionStorage value existence', () => {
const isChunkError = handleChunkError(testChunkErrorObj)

expect(sessionStorage.getItem(LAST_CHUNK_FAILURE_RELOAD_KEY)).not.toBeNull()
expect(isChunkError).toBe(true)
expect(window.location.reload).toHaveBeenCalled()
expect(sessionStorage.getItem(LAST_CHUNK_FAILURE_RELOAD_KEY)).toBeNull()
expect(isChunkError).toBe(false)
expect(window.location.reload).not.toHaveBeenCalled()
})

it('handles malformed sessionStorage values', () => {
Expand Down
12 changes: 7 additions & 5 deletions src/components/GlobalErrorBoundary/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Text, Link, Icon, FixedIcon, Title } from '@gnosis.pm/safe-react-compon
import { IS_PRODUCTION } from 'src/utils/constants'
import { FallbackRender } from '@sentry/react/dist/errorboundary'
import { ROOT_ROUTE } from 'src/routes/routes'
import { loadFromSessionStorage, removeFromSessionStorage, saveToSessionStorage } from 'src/utils/storage/session'

const Wrapper = styled.div`
width: 100%;
Expand Down Expand Up @@ -53,12 +54,13 @@ export const handleChunkError = (error: Error): boolean => {

if (!isChunkError) return false

const lastReloadString = sessionStorage.getItem(LAST_CHUNK_FAILURE_RELOAD_KEY)
const lastReload = lastReloadString ? +lastReloadString : 0
const lastReload = loadFromSessionStorage<number>(LAST_CHUNK_FAILURE_RELOAD_KEY)

const isTimestamp = typeof lastReload === 'number' && !isNaN(lastReload)

// Not a number in the sessionStorage
if (isNaN(lastReload)) {
sessionStorage.removeItem(LAST_CHUNK_FAILURE_RELOAD_KEY)
if (!isTimestamp) {
removeFromSessionStorage(LAST_CHUNK_FAILURE_RELOAD_KEY)
return false
}

Expand All @@ -68,7 +70,7 @@ export const handleChunkError = (error: Error): boolean => {

if (hasJustReloaded) return false

sessionStorage.setItem(LAST_CHUNK_FAILURE_RELOAD_KEY, now.toString())
saveToSessionStorage(LAST_CHUNK_FAILURE_RELOAD_KEY, now.toString())
window.location.reload()
return true
}
Expand Down
8 changes: 5 additions & 3 deletions src/components/StoreMigrator/__test__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('getNetworksToMigrate', () => {
})

const networks = migrationUtils.getNetworksToMigrate()
expect(networks).toEqual(['polygon', 'bsc', 'rinkeby', 'xdai', 'ewc', 'volta'])
expect(networks).toEqual(['arbitrum', 'bsc', 'ewc', 'polygon', 'rinkeby', 'volta', 'xdai'])
})

it('returns non-migrated networks', () => {
Expand All @@ -64,13 +64,15 @@ describe('getNetworksToMigrate', () => {
})

const networks = migrationUtils.getNetworksToMigrate()
expect(networks).toEqual(['polygon', 'xdai', 'ewc', 'volta'])
expect(networks).toEqual(['arbitrum', 'ewc', 'polygon', 'volta', 'xdai'])
})

it('returns an empty array when all networks are migrated', () => {
Object.defineProperty(window, 'localStorage', {
writable: true,
value: { getItem: jest.fn(() => JSON.stringify(['polygon', 'bsc', 'rinkeby', 'xdai', 'ewc', 'volta'])) },
value: {
getItem: jest.fn(() => JSON.stringify(['arbitrum', 'bsc', 'polygon', 'rinkeby', 'xdai', 'ewc', 'volta'])),
},
})

const networks = migrationUtils.getNetworksToMigrate()
Expand Down
2 changes: 1 addition & 1 deletion src/components/StoreMigrator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const ADDRESS_BOOK_KEY = 'SAFE__addressBook'
const IMMORTAL_PREFIX = '_immortal|'
const MAINNET_PREFIX = 'MAINNET'

const networks = ['polygon', 'bsc', 'rinkeby', 'xdai', 'ewc', 'volta'] as const
const networks = ['arbitrum', 'bsc', 'ewc', 'polygon', 'rinkeby', 'volta', 'xdai'] as const
export type NETWORK_TO_MIGRATE = typeof networks[number]

export function getSubdomainUrl(network: NETWORK_TO_MIGRATE): string {
Expand Down
102 changes: 66 additions & 36 deletions src/components/forms/AddressInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react'
import { Field } from 'react-final-form'
import { OnChange } from 'react-final-form-listeners'
import { ReactElement, useEffect, useState } from 'react'
import { Field, useFormState } from 'react-final-form'
import InputAdornment from '@material-ui/core/InputAdornment'
import CircularProgress from '@material-ui/core/CircularProgress'

import TextField from 'src/components/forms/TextField'
import { Validator, composeValidators, mustBeEthereumAddress, required } from 'src/components/forms/validator'
Expand All @@ -19,7 +20,7 @@ export interface AddressInputProps {
name?: string
text?: string
placeholder?: string
inputAdornment?: { endAdornment: React.ReactElement } | undefined | false
inputAdornment?: { endAdornment: ReactElement } | undefined | false
testId: string
validators?: Validator[]
defaultValue?: string
Expand All @@ -39,14 +40,70 @@ const AddressInput = ({
validators = [],
defaultValue,
disabled,
}: AddressInputProps): React.ReactElement => (
<>
}: AddressInputProps): ReactElement => {
const { values } = useFormState()
const address = trimSpaces(values[name])

const [showLoadingSpinner, setShowLoadingSpinner] = useState<boolean>(false)

useEffect(() => {
let isCurrentResolution = true
const handleAddressInput = async () => {
// A crypto domain name
if (isValidEnsName(address) || isValidCryptoDomainName(address)) {
setShowLoadingSpinner(true)
// Trigger resolution/loading spinner
try {
const resolverAddr = await getAddressFromDomain(address)
const formattedAddress = checksumAddress(resolverAddr)

// Set field if current resolution in current effect
if (isCurrentResolution) {
fieldMutator(formattedAddress)
}
} catch (err) {
logError(Errors._101, err.message)
} finally {
setShowLoadingSpinner(false)
}
} else {
// A regular address hash// A regular address hash
let checkedAddress = address

// Automatically checksum valid (either already checksummed, or lowercase addresses)
if (isValidAddress(address)) {
try {
checkedAddress = checksumAddress(address)
} catch {}
}
fieldMutator(checkedAddress)
}
}
handleAddressInput()

return () => {
// Effect is no longer current when address changes
isCurrentResolution = false
}
}, [address, fieldMutator])

const adornment = showLoadingSpinner
? {
endAdornment: (
<InputAdornment position="end">
<CircularProgress size="16px" />
</InputAdornment>
),
}
: inputAdornment

return (
<Field
className={className}
component={TextField as any}
defaultValue={defaultValue}
disabled={disabled}
inputAdornment={inputAdornment}
inputAdornment={adornment}
name={name}
placeholder={placeholder}
testId={testId}
Expand All @@ -55,34 +112,7 @@ const AddressInput = ({
spellCheck={false}
validate={composeValidators(required, mustBeEthereumAddress, ...validators)}
/>
<OnChange name={name}>
{async (value: string) => {
const address = trimSpaces(value)
// A crypto domain name
if (isValidEnsName(address) || isValidCryptoDomainName(address)) {
try {
const resolverAddr = await getAddressFromDomain(address)
const formattedAddress = checksumAddress(resolverAddr)
fieldMutator(formattedAddress)
} catch (err) {
logError(Errors._101, err.message)
}
} else {
// A regular address hash
let checkedAddress = address
// Automatically checksum valid (either already checksummed, or lowercase addresses)
if (isValidAddress(address)) {
try {
checkedAddress = checksumAddress(address)
} catch (err) {
// ignore
}
}
fieldMutator(checkedAddress)
}
}}
</OnChange>
</>
)
)
}

export default AddressInput
Loading

0 comments on commit 16cc7cd

Please sign in to comment.