diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a24d3ea427..028a1d0fe9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,13 +38,25 @@ jobs: - name: tests run: | npm run test - E2E_tests_dApp_Chrome: - if: github.event.review && (github.event.review.state == 'approved' || contains(github.event.review.body, '/check')) - runs-on: macos-11 + + E2E_tests_dApp: + if: github.event.review && (github.event.review.state == 'approved' || contains(github.event.review.body, '/check') || contains(github.event.review.body, '/dapp-check')) + runs-on: macos-12 + strategy: + matrix: + browser: ['chrome', 'firefox'] + fail-fast: false steps: - uses: actions/checkout@v2 - name: Forcefully update the Chrome browser + if: matrix.browser=='chrome' run: brew update && brew upgrade --cask google-chrome + - name: Forcefully install Firefox for Developers browser + if: matrix.browser=='firefox' + run: | + brew update + brew tap homebrew/cask-versions && brew install --cask firefox-developer-edition + echo "FIREFOX_DEV=/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox-bin" >> $GITHUB_ENV - name: Read .nvmrc run: echo ::set-output name=NVMRC::$(cat .nvmrc) id: nvm @@ -97,7 +109,7 @@ jobs: working-directory: ./packages/yoroi-extension env: MAILSAC_API_KEY: ${{ secrets.MAILSAC_API_KEY }} - run: npm run test:run:e2e:dApp:chrome + run: npm run test:run:e2e:dApp:${{ matrix.browser }} - name: Archive tests screenshots and logs if: ${{ failure() }} uses: actions/upload-artifact@v3 @@ -105,25 +117,77 @@ jobs: name: testRunsData path: ./packages/yoroi-extension/testRunsData - E2E_tests_dApp_FF: - if: github.event.review && (github.event.review.state == 'approved' || contains(github.event.review.body, '/check') || contains(github.event.review.body, '/run_dapp_ff')) - runs-on: macos-11 + Trezor_Model_T_emulator: + if: github.event.review && (github.event.review.state == 'approved' || contains(github.event.review.body, '/check') || contains(github.event.review.body, '/trezor-check')) + runs-on: ubuntu-22.04 + strategy: + matrix: + browser: [ 'chrome', 'firefox' ] + fail-fast: false steps: - - uses: actions/checkout@v2 - - name: Forcefully install Firefox for Developers browser - run: brew update && brew tap homebrew/cask-versions && brew install --cask firefox-developer-edition - - name: Set up env variable for FF - run: echo "FIREFOX_DEV=/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox-bin" >> $GITHUB_ENV + - name: Forcefully update the Chrome browser + if: matrix.browser=='chrome' + run: | + wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + sudo sh -c 'echo "deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' + sudo apt-get update + sudo apt-get --only-upgrade install google-chrome-stable + + - name: Install Firefox Developer Edition + if: matrix.browser=='firefox' + run: | + wget -c "https://download.mozilla.org/?product=firefox-devedition-latest-ssl&os=linux64&lang=en-US" -O - | sudo tar -xj -C /opt + sudo rm -rf /opt/firefoxdev + sudo mv /opt/firefox /opt/firefoxdev + echo "FIREFOX_DEV=/opt/firefoxdev/firefox-bin" >> $GITHUB_ENV + + - name: Clone the trezor-usr-env + run: | + cd .. + git clone https://github.com/trezor/trezor-user-env.git + + - name: Install nix-shell + run: | + sudo apt update + sudo apt -y install nix-bin + + - name: Download firmware + working-directory: ../trezor-user-env/src/binaries/firmware/bin/ + run: sudo ./download.sh + + - name: Download trezord-go + working-directory: ../trezor-user-env/src/binaries/trezord-go/bin/ + run: sudo ./download.sh + + - name: Copy the v2-master firmware to the root of the project + working-directory: ../trezor-user-env + run: cp -rf src/binaries/firmware/bin/trezor-emu-core-v2-master ./ + + - name: Copy the trezord-go-v2.0.31 to the root of the project + working-directory: ../trezor-user-env + run: cp -rf src/binaries/trezord-go/bin/trezord-go-v2.0.31 ./ + + - name: Create the logs folder in the root of the trezor-user-env + working-directory: ../trezor-user-env + run: mkdir logs + + - name: Run the trezor user environment + working-directory: ../trezor-user-env + run: sudo docker run -p 9001:9001 -p 9002:9002 -p 21326:21326 -p 127.0.0.1:21325:21326 -p 21324:21324 -v logs:/trezor-user-env/logs/screens -v trezor-emu-core-v2-master:/trezor-user-env/src/binaries/firmware/bin/user_downloaded -v trezord-go-v2.0.31:/trezor-user-env/src/binaries/trezord-go/bin -d emurgornd/trezor-user-env:latest + + - uses: actions/checkout@v3 + - name: Read .nvmrc run: echo ::set-output name=NVMRC::$(cat .nvmrc) id: nvm + - name: Setup node - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: '${{ steps.nvm.outputs.NVMRC }}' - name: Cache extension node modules # https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows - uses: actions/cache@v2 + uses: actions/cache@v3 env: cache-name: cache-yoroi-extension-node-modules with: @@ -136,9 +200,10 @@ jobs: ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- + - name: Cache connector node modules # https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows - uses: actions/cache@v2 + uses: actions/cache@v3 env: cache-name: cache-yoroi-connector-node-modules with: @@ -151,24 +216,25 @@ jobs: ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- - - name: Clean cache - run: npm cache clean -f + - name: npm install run: | . install-all.sh - - name: Build the test version + + - name: Build the test version using emulators working-directory: ./packages/yoroi-extension run: npm run test:build + - name: Create the report's folder working-directory: ./packages/yoroi-extension run: | mkdir reports touch ./reports/cucumberReports.json - - name: Run dapp connector tests + + - name: Run tests working-directory: ./packages/yoroi-extension - env: - MAILSAC_API_KEY: ${{ secrets.MAILSAC_API_KEY }} - run: npm run test:run:e2e:dApp:firefox + run: xvfb-run -a -e /dev/stdout -s "-screen 0 1920x1080x24" npm run test:run:e2e:trezor:${{ matrix.browser }} + - name: Archive tests screenshots and logs if: ${{ failure() }} uses: actions/upload-artifact@v3 diff --git a/.github/workflows/tests_hw_emulators.yml b/.github/workflows/tests_hw_emulators.yml deleted file mode 100644 index 5e198109bf..0000000000 --- a/.github/workflows/tests_hw_emulators.yml +++ /dev/null @@ -1,246 +0,0 @@ -name: Tests with HW emulators - -on: - pull_request_review: - types: [ submitted ] - -jobs: - Trezor_Model_T_Chrome: - if: github.event.review && (github.event.review.state == 'approved' || contains(github.event.review.body, '/check')) - runs-on: ubuntu-22.04 - - steps: - - name: Forcefully update the Chrome browser - run: | - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - - sudo sh -c 'echo "deb [arch=amd64] https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' - sudo apt-get update - sudo apt-get --only-upgrade install google-chrome-stable - - - name: Clone the trezor-usr-env - run: | - cd .. - git clone https://github.com/trezor/trezor-user-env.git - - - name: Install nix-shell - run: | - sudo apt update - sudo apt -y install nix-bin - - - name: Download firmware - working-directory: ../trezor-user-env/src/binaries/firmware/bin/ - run: | - sed '/^.*R_LATEST_BUILD.*$/d' download.sh | sed '/^.*trezor_r_master.*$/d' > temp_file.sh - rm download.sh - mv temp_file.sh download.sh - chmod +x download.sh - sudo ./download.sh - - - name: Download trezord-go - working-directory: ../trezor-user-env - run: sudo ./src/binaries/trezord-go/bin/download.sh - - - name: Copy the v2-master firmware to the root of the project - working-directory: ../trezor-user-env - run: cp -rf src/binaries/firmware/bin/trezor-emu-core-v2-master ./ - - - name: Copy the trezord-go-v2.0.31 to the root of the project - working-directory: ../trezor-user-env - run: cp -rf src/binaries/trezord-go/bin/trezord-go-v2.0.31 ./ - - - name: Create the logs folder in the root of the trezor-user-env - working-directory: ../trezor-user-env - run: mkdir logs - - - name: Run the trezor user environment - working-directory: ../trezor-user-env - run: sudo docker run -p 9001:9001 -p 9002:9002 -p 21326:21326 -p 127.0.0.1:21325:21326 -p 21324:21324 -v logs:/trezor-user-env/logs/screens -v trezor-emu-core-v2-master:/trezor-user-env/src/binaries/firmware/bin/user_downloaded -v trezord-go-v2.0.31:/trezor-user-env/src/binaries/trezord-go/bin -d emurgornd/trezor-user-env:latest - - - uses: actions/checkout@v3 - - - name: Read .nvmrc - run: echo ::set-output name=NVMRC::$(cat .nvmrc) - id: nvm - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: '${{ steps.nvm.outputs.NVMRC }}' - - - name: Cache extension node modules - # https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows - uses: actions/cache@v3 - env: - cache-name: cache-yoroi-extension-node-modules - with: - # https://github.com/actions/cache/blob/main/examples.md#node---npm - # It is recommended to cache the NPM cache (~/.npm) instead of node_modules. - # But we put node version into the cache key and cache node_modules. - path: packages/yoroi-extension/node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-node-${{ steps.nvm.outputs.NVMRC }}-${{ hashFiles('packages/yoroi-extension/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - name: Cache connector node modules - # https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows - uses: actions/cache@v3 - env: - cache-name: cache-yoroi-connector-node-modules - with: - # https://github.com/actions/cache/blob/main/examples.md#node---npm - # It is recommended to cache the NPM cache (~/.npm) instead of node_modules. - # But we put node version into the cache key and cache node_modules. - path: packages/yoroi-ergo-connector/node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-node-${{ steps.nvm.outputs.NVMRC }}-${{ hashFiles('packages/yoroi-ergo-connector/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - name: npm install - run: | - . install-all.sh - - - name: Build the test version using emulators - working-directory: ./packages/yoroi-extension - run: npm run test:build - - - name: Create the report's folder - working-directory: ./packages/yoroi-extension - run: | - mkdir reports - touch ./reports/cucumberReports.json - - - name: Run tests - working-directory: ./packages/yoroi-extension - run: xvfb-run -a -e /dev/stdout -s "-screen 0 1920x1080x24" npm run test:run:e2e:trezor:chrome - - - name: Archive tests screenshots and logs - if: ${{ failure() }} - uses: actions/upload-artifact@v3 - with: - name: testRunsData - path: ./packages/yoroi-extension/testRunsData - - Trezor_Model_T_Firefox: - if: github.event.review && (github.event.review.state == 'approved' || contains(github.event.review.body, '/check')) - runs-on: ubuntu-22.04 - - steps: - - name: Install Firefox Developer Edition - run: | - wget -c "https://download.mozilla.org/?product=firefox-devedition-latest-ssl&os=linux64&lang=en-US" -O - | sudo tar -xj -C /opt - sudo rm -rf /opt/firefoxdev - sudo mv /opt/firefox /opt/firefoxdev - - - name: Set up env variable for FF - run: echo "FIREFOX_DEV=/opt/firefoxdev/firefox-bin" >> $GITHUB_ENV - - - name: Clone the trezor-usr-env - run: | - cd .. - git clone https://github.com/trezor/trezor-user-env.git - - - name: Install nix-shell - run: | - sudo apt update - sudo apt -y install nix-bin - - - name: Download firmware - working-directory: ../trezor-user-env/src/binaries/firmware/bin/ - run: | - sed '/^.*R_LATEST_BUILD.*$/d' download.sh | sed '/^.*trezor_r_master.*$/d' > temp_file.sh - rm download.sh - mv temp_file.sh download.sh - chmod +x download.sh - sudo ./download.sh - - - name: Download trezord-go - working-directory: ../trezor-user-env - run: sudo ./src/binaries/trezord-go/bin/download.sh - - - name: Copy the v2-master firmware to the root of the project - working-directory: ../trezor-user-env - run: cp -rf src/binaries/firmware/bin/trezor-emu-core-v2-master ./ - - - name: Copy the trezord-go-v2.0.31 to the root of the project - working-directory: ../trezor-user-env - run: cp -rf src/binaries/trezord-go/bin/trezord-go-v2.0.31 ./ - - - name: Create the logs folder in the root of the trezor-user-env - working-directory: ../trezor-user-env - run: mkdir logs - - - name: Run the trezor user environment - working-directory: ../trezor-user-env - run: sudo docker run -p 9001:9001 -p 9002:9002 -p 21326:21326 -p 127.0.0.1:21325:21326 -p 21324:21324 -v logs:/trezor-user-env/logs/screens -v trezor-emu-core-v2-master:/trezor-user-env/src/binaries/firmware/bin/user_downloaded -v trezord-go-v2.0.31:/trezor-user-env/src/binaries/trezord-go/bin -d emurgornd/trezor-user-env:latest - - - uses: actions/checkout@v3 - - - name: Read .nvmrc - run: echo ::set-output name=NVMRC::$(cat .nvmrc) - id: nvm - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: '${{ steps.nvm.outputs.NVMRC }}' - - - name: Cache extension node modules - # https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows - uses: actions/cache@v3 - env: - cache-name: cache-yoroi-extension-node-modules - with: - # https://github.com/actions/cache/blob/main/examples.md#node---npm - # It is recommended to cache the NPM cache (~/.npm) instead of node_modules. - # But we put node version into the cache key and cache node_modules. - path: packages/yoroi-extension/node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-node-${{ steps.nvm.outputs.NVMRC }}-${{ hashFiles('packages/yoroi-extension/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - name: Cache connector node modules - # https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows - uses: actions/cache@v3 - env: - cache-name: cache-yoroi-connector-node-modules - with: - # https://github.com/actions/cache/blob/main/examples.md#node---npm - # It is recommended to cache the NPM cache (~/.npm) instead of node_modules. - # But we put node version into the cache key and cache node_modules. - path: packages/yoroi-ergo-connector/node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-node-${{ steps.nvm.outputs.NVMRC }}-${{ hashFiles('packages/yoroi-ergo-connector/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - name: npm install - run: | - . install-all.sh - - - name: Build the test version using emulators - working-directory: ./packages/yoroi-extension - run: npm run test:build - - - name: Create the report's folder - working-directory: ./packages/yoroi-extension - run: | - mkdir reports - touch ./reports/cucumberReports.json - - - name: Run tests with the trezor emulator - working-directory: ./packages/yoroi-extension - run: xvfb-run -a -e /dev/stdout -s "-screen 0 1920x1080x24" npm run test:run:e2e:trezor:firefox - - - name: Archive tests screenshots and logs - if: ${{ failure() }} - uses: actions/upload-artifact@v3 - with: - name: testRunsData - path: ./packages/yoroi-extension/testRunsData \ No newline at end of file diff --git a/packages/yoroi-ergo-connector/example-cardano/package-lock.json b/packages/yoroi-ergo-connector/example-cardano/package-lock.json index d790d2bbb0..db480a54d7 100644 --- a/packages/yoroi-ergo-connector/example-cardano/package-lock.json +++ b/packages/yoroi-ergo-connector/example-cardano/package-lock.json @@ -10,9 +10,9 @@ "integrity": "sha512-iGDh2M6pFuXg9kyW+U//963LKylSLFpLG5hZvUppCjhkiDwsYquQPyamxCQlLASYySS3gGKAki2eWG9qIHKCew==" }, "@emurgo/cardano-serialization-lib-browser": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-browser/-/cardano-serialization-lib-browser-10.2.0.tgz", - "integrity": "sha512-b4RrWtC5y8+bjKp9sFo8IWXg+xIatXoUq4YcqFDReDN6dpHRjLlOJv7h9Da6VA5GXvLQy6vOFuieAZJFGng9FQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-browser/-/cardano-serialization-lib-browser-11.0.0.tgz", + "integrity": "sha512-aJHJSUdlXGu1wfr7sETdoE6mv43jdtflW03Jwro3kFQwchLGAOUiA4LfTDOKwgrtxT6lq5fQW46YoHh14PZDyg==", "dev": true }, "@emurgo/cip4-js": { diff --git a/packages/yoroi-ergo-connector/example-cardano/package.json b/packages/yoroi-ergo-connector/example-cardano/package.json index 51faf6e82f..e8afd68634 100644 --- a/packages/yoroi-ergo-connector/example-cardano/package.json +++ b/packages/yoroi-ergo-connector/example-cardano/package.json @@ -11,7 +11,7 @@ "start": "webpack-dev-server" }, "devDependencies": { - "@emurgo/cardano-serialization-lib-browser": "10.2.0", + "@emurgo/cardano-serialization-lib-browser": "11.0.0", "webpack": "^4.29.3", "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.1.5", diff --git a/packages/yoroi-extension/app/App.js b/packages/yoroi-extension/app/App.js index e8ace513c7..1e9e2581a5 100644 --- a/packages/yoroi-extension/app/App.js +++ b/packages/yoroi-extension/app/App.js @@ -36,6 +36,7 @@ import { ThemeProvider } from '@mui/material/styles'; import { CssBaseline } from '@mui/material'; import { globalStyles } from './styles/globalStyles'; import Support from './components/widgets/Support'; +import { trackNavigation } from './api/analytics'; // https://github.com/yahoo/react-intl/wiki#loading-locale-data addLocaleData([ @@ -80,6 +81,9 @@ class App extends Component { this.mergedMessages = _mergedMessages; }); }); + this.props.history.listen(({ pathname }) => { + trackNavigation(pathname); + }); } state: State = { diff --git a/packages/yoroi-extension/app/api/ada/index.js b/packages/yoroi-extension/app/api/ada/index.js index 782aa65fe6..76d7ee7ea0 100644 --- a/packages/yoroi-extension/app/api/ada/index.js +++ b/packages/yoroi-extension/app/api/ada/index.js @@ -1351,7 +1351,7 @@ export default class AdaApi { } } - let amount = makeMultiToken(target.value ?? '0'); + let amount = makeMultiToken(target.value ?? '1000000'); const dataHash = target.dataHash; const ensureMinValue = target.ensureRequiredMinimalValue; if (ensureMinValue == null || ensureMinValue === false) { diff --git a/packages/yoroi-extension/app/api/ada/transactions/shelley/coinSelection.test.js b/packages/yoroi-extension/app/api/ada/transactions/shelley/coinSelection.test.js index bd7c019297..bc5264d5da 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/coinSelection.test.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/coinSelection.test.js @@ -496,7 +496,7 @@ describe('takeUtxosForValues', () => { // The third utxo is taken then const take2 = CoinSelection.takeUtxosForValues( [utxos[2], utxos[0], utxos[1]], - [multiToken(1_990_000)], + [multiToken(2_990_000)], createCoinsPerUtxoWord(30_000), 0, ); diff --git a/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.js b/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.js index 0649a96178..83d64cd552 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.js @@ -1042,7 +1042,9 @@ function newAdaUnsignedTxFromUtxoForConnector( txBuilder.set_collateral(collateralBuilder); // script data hash txBuilder.calc_script_data_hash( - RustModule.WalletV4.TxBuilderConstants.plutus_default_cost_models(), + protocolParams.networkId > 0 ? + RustModule.WalletV4.TxBuilderConstants.plutus_vasil_cost_models() + : RustModule.WalletV4.TxBuilderConstants.plutus_default_cost_models(), ); } diff --git a/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.test.js b/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.test.js index 9129f7be4d..6c9079b917 100644 --- a/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.test.js +++ b/packages/yoroi-extension/app/api/ada/transactions/shelley/transactions.test.js @@ -493,8 +493,8 @@ describe('Create unsigned TX from UTXO', () => { // it will take only one of the utxos because it covers the required token and the fee expect(unsignedTxResponse.senderUtxos).toEqual([utxos[4], utxos[2]]); expect(unsignedTxResponse.txBuilder.get_explicit_input().coin().to_str()).toEqual('12000002'); - expect(unsignedTxResponse.txBuilder.get_explicit_output().coin().to_str()).toEqual('11998056'); - expect(unsignedTxResponse.txBuilder.min_fee().to_str()).toEqual('1946'); + expect(unsignedTxResponse.txBuilder.get_explicit_output().coin().to_str()).toEqual('11998058'); + expect(unsignedTxResponse.txBuilder.min_fee().to_str()).toEqual('1944'); const assetInfo = identifierToCardanoAsset(testAssetId); expect(unsignedTxResponse.txBuilder.get_explicit_input().multiasset() diff --git a/packages/yoroi-extension/app/api/analytics/index.js b/packages/yoroi-extension/app/api/analytics/index.js new file mode 100644 index 0000000000..73f063a9bd --- /dev/null +++ b/packages/yoroi-extension/app/api/analytics/index.js @@ -0,0 +1,171 @@ +// @flow +import cryptoRandomString from 'crypto-random-string'; +import querystring from 'querystring'; + +import LocalStorageApi, { + loadAnalyticsInstanceId, + saveAnalyticsInstanceId, +} from '../localStorage'; +import { environment } from '../../environment'; +import { TRACKED_ROUTES } from '../../routes-config'; +import type { StoresMap } from '../../stores'; +import { isTestnet as isTestnetFunc } from '../ada/lib/storage/database/prepackaged/networks'; + +const MATOMO_URL = 'https://analytics.emurgo-rnd.com/matomo.php'; +const SITE_ID = '4'; +let INSTANCE_ID; +let stores; + +export async function trackStartup(stores_: StoresMap): Promise { + stores = stores_; + + let event; + if (await (new LocalStorageApi()).getUserLocale() != null) { + INSTANCE_ID = await loadAnalyticsInstanceId(); + if (INSTANCE_ID) { + emitEvent(INSTANCE_ID, 'launch'); + return; + } + event = 'pre-existing-instance'; + } else { + event = 'new-instance'; + } + INSTANCE_ID = generateAnalyticsInstanceId(); + await saveAnalyticsInstanceId(INSTANCE_ID); + emitEvent(INSTANCE_ID, event); +} + +type NewWalletType = 'hardware' | 'created' | 'restored'; + +export function trackWalletCreation(newWalletType: NewWalletType): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'new-wallet/' + newWalletType); +} + +export function trackNavigation(path: string): void { + if (path.match(TRACKED_ROUTES)) { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'navigate' + path); + } +} + +export function trackSend(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'new-transaction/send'); +} + +export function trackDelegation(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'delegation'); +} + +export function trackWithdrawal(shouldDeregister: boolean): void { + if (INSTANCE_ID == null) { + return; + } + if (shouldDeregister) { + emitEvent(INSTANCE_ID, 'deregister'); + } else { + emitEvent(INSTANCE_ID, 'withdrawal'); + } +} + +export function trackCatalystRegistration(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'new-transaction/catalyst'); +} + +export function trackSetLocale(locale: string): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'set-locale/' + locale); +} + +export function trackSetUnitOfAccount(unitOfAccount: string): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'unit-of-account/' + unitOfAccount); +} + +export function trackUpdateTheme(theme: string): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'update-theme/' + theme); +} + +export function trackUriPrompt(choice: 'skip' | 'allow'): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'uri-prompt/' + choice); +} + +export function trackBuySellDialog(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'buy-sell-ada'); +} + +export function trackExportWallet(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'export-wallet'); +} + +export function trackRemoveWallet(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'remove-wallet'); +} + +export function trackResyncWallet(): void { + if (INSTANCE_ID == null) { + return; + } + emitEvent(INSTANCE_ID, 'resync-wallet'); +} + +function generateAnalyticsInstanceId(): string { + // Matomo requires 16 character hex string + return cryptoRandomString({ length: 16 }); +} + +function emitEvent(instanceId: string, event: string): void { + if (environment.isDev() || environment.isTest()) { + return; + } + + const isTestnet = stores.profile.selectedNetwork != null ? + isTestnetFunc(stores.profile.selectedNetwork) : + false; + + // https://developer.matomo.org/api-reference/tracking-api + const params = { + idsite: SITE_ID, + rec: '1', + action_name: (isTestnet ? 'testnet/' : '') + event, + url: `yoroi.extension/${isTestnet ? 'testnet/' : ''}${event}`, + _id: INSTANCE_ID, + rand: `${Date.now()}-${Math.random()}`, + apiv: '1' + }; + const url = `${MATOMO_URL}?${querystring.stringify(params)}`; + + fetch(url); +} diff --git a/packages/yoroi-extension/app/api/common/errors.js b/packages/yoroi-extension/app/api/common/errors.js index 7f205dfaaf..da31e0dc5a 100644 --- a/packages/yoroi-extension/app/api/common/errors.js +++ b/packages/yoroi-extension/app/api/common/errors.js @@ -124,6 +124,10 @@ const messages = defineMessages({ id: 'api.errors.checkAdressesInUseApiError', defaultMessage: '!!!Error received from server while checking used addresses.', }, + getMultiAssetMintMetadataApiError: { + id: 'api.errors.getMultiAssetMintMetadataApiError', + defaultMessage: '!!!Error received from server while querying minting metadata.', + }, assetInfoApiError: { id: 'api.errors.assetInfoApiError', defaultMessage: '!!!Error received from server while getting asset info.', @@ -439,6 +443,15 @@ export class CheckAddressesInUseApiError extends LocalizableError { } } +export class GetMultiAssetMintMetadataApiError extends LocalizableError { + constructor() { + super({ + id: messages.getMultiAssetMintMetadataApiError.id, + defaultMessage: messages.getMultiAssetMintMetadataApiError.defaultMessage || '', + }); + } +} + export class GetAssetInfoApiError extends LocalizableError { constructor() { super({ diff --git a/packages/yoroi-extension/app/api/localStorage/index.js b/packages/yoroi-extension/app/api/localStorage/index.js index efdc33bab5..d183008186 100644 --- a/packages/yoroi-extension/app/api/localStorage/index.js +++ b/packages/yoroi-extension/app/api/localStorage/index.js @@ -32,6 +32,7 @@ const storageKeys = { TOGGLE_SIDEBAR: networkForLocalStorage + '-TOGGLE-SIDEBAR', WALLETS_NAVIGATION: networkForLocalStorage + '-WALLETS-NAVIGATION', SUBMITTED_TRANSACTIONS: 'submittedTransactions', + ANALYTICS_INSTANCE_ID: networkForLocalStorage + '-ANALYTICS', // ========== CONNECTOR ========== // ERGO_CONNECTOR_WHITELIST: 'connector_whitelist', }; @@ -360,3 +361,12 @@ export function loadSubmittedTransactions(): any { } return JSON.parse(dataStr); } + +export async function loadAnalyticsInstanceId(): Promise { + return getLocalItem(storageKeys.ANALYTICS_INSTANCE_ID); +} + +export async function saveAnalyticsInstanceId(id: string): Promise { + await setLocalItem(storageKeys.ANALYTICS_INSTANCE_ID, id); +} + diff --git a/packages/yoroi-extension/app/components/buySell/BuySellDialog.js b/packages/yoroi-extension/app/components/buySell/BuySellDialog.js index fe8a5749ff..a3497124d6 100644 --- a/packages/yoroi-extension/app/components/buySell/BuySellDialog.js +++ b/packages/yoroi-extension/app/components/buySell/BuySellDialog.js @@ -15,6 +15,7 @@ import { ReactComponent as VerifyIcon } from '../../assets/images/verify-icon.i import VerticalFlexContainer from '../layout/VerticalFlexContainer' import LoadingSpinner from '../widgets/LoadingSpinner' import globalMessages from '../../i18n/global-messages'; +import { trackBuySellDialog } from '../../api/analytics'; const messages = defineMessages({ dialogTitle: { @@ -77,6 +78,7 @@ export default class BuySellDialog extends Component { } ] this.setState({ walletList: wallets }) + trackBuySellDialog(); } createRows: ($npm$ReactIntl$IntlFormat, Array) => Node = (intl, wallets) => ( diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js index ec1691b59b..6fe88ad8ed 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer.js @@ -20,6 +20,7 @@ import { } from '../../../../api/ada/lib/storage/models/ConceptualWallet'; import type { SendUsingLedgerParams } from '../../../../actions/ada/ledger-send-actions'; import type { SendUsingTrezorParams } from '../../../../actions/ada/trezor-send-actions'; +import { trackSend } from '../../../../api/analytics'; export type GeneratedData = typeof WalletSendPreviewStepContainer.prototype.generated; @@ -90,6 +91,7 @@ export default class WalletSendPreviewStepContainer extends Component { onSuccess: openTransactionSuccessDialog, }); } + trackSend() } render(): Node { diff --git a/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/EpochProgressCard.js b/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/EpochProgressCard.js new file mode 100644 index 0000000000..4f5f94ff50 --- /dev/null +++ b/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/EpochProgressCard.js @@ -0,0 +1,113 @@ +// @flow +import type { Node } from 'react'; +import { Box } from '@mui/system'; +import { CircularProgress, Stack, Typography } from '@mui/material'; + +type Props = {| + +percentage: number, + +days: string, + +currentEpoch: number, + +startEpochDate: string, + +endEpochDate: string, +|}; + +export function EpochProgressCard({ + percentage, + days, + currentEpoch, + startEpochDate, + endEpochDate, +}: Props): Node { + return ( + + + + + + <Stack direction="row" spacing={3} mt="50px" justifyContent="space-between"> + <LabelWithValue label="Epoch started at" value={startEpochDate} /> + <LabelWithValue label="Epoch ends at" value={endEpochDate} /> + </Stack> + </Stack> + </Stack> + </Box> + ); +} + +type TitleProps = {| + +label: string, + +value: string | number, +|}; +const Title = ({ label, value }: TitleProps): Node => { + return ( + <Box> + <Typography fontWeight="500" color="var(--yoroi-palette-primary-300)"> + {label}: {value} + </Typography> + </Box> + ); +}; + +type InfoColumnProps = {| + +label: string, + +value: string | number, +|}; +const LabelWithValue = ({ label, value }: InfoColumnProps): Node => { + return ( + <Box> + <Typography + style={{ textTransform: 'uppercase' }} + variant="caption" + mb="4px" + color="var(--yoroi-palette-gray-600)" + > + {label} + </Typography> + <Typography color="var(--yoroi-palette-gray-900)">{value}</Typography> + </Box> + ); +}; + +const Graph = ({ value, days }): Node => { + return ( + <Box mr="8px" position="relative" display="flex" justifyContent="center"> + <CircularProgress + size={120} + thickness={7} + variant="determinate" + value={value} + sx={{ + color: 'var(--yoroi-palette-primary-300)', + animationDuration: '550ms', + position: 'absolute', + zIndex: 1, + }} + /> + <CircularProgress + size={120} + thickness={7} + variant="determinate" + sx={{ + color: 'var(--yoroi-palette-gray-50)', + }} + value={100} + /> + <Box + position="absolute" + sx={{ + top: '30%', + left: '50%', + transform: 'translate(-50%)', + textAlign: 'center', + }} + > + <Typography variant="h4" color="var(--yoroi-palette-gray-900)"> + {value}% + </Typography> + <Typography variant="caption1" fontSize="12px" color="var(--yoroi-palette-gray-600)"> + {days} days + </Typography> + </Box> + </Box> + ); +}; diff --git a/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/StakingTabs.js b/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/StakingTabs.js index ed0cd4a6e6..7e5aec8d32 100644 --- a/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/StakingTabs.js +++ b/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/StakingTabs.js @@ -5,8 +5,8 @@ import { Box, styled } from '@mui/system'; import { TabContext, TabList, TabPanel } from '@mui/lab'; import { IconButton, Tab, Typography } from '@mui/material'; import { observer } from 'mobx-react'; -import { ReactComponent as InfoIconSVG } from '../../../../assets/images/info-icon.inline.svg'; -import { ReactComponent as CloseIcon } from '../../../../assets/images/forms/close.inline.svg'; +import { ReactComponent as InfoIconSVG } from '../../../../assets/images/info-icon.inline.svg'; +import { ReactComponent as CloseIcon } from '../../../../assets/images/forms/close.inline.svg'; import DelegatedStakePoolCard from './DelegatedStakePoolCard'; import { defineMessages, injectIntl } from 'react-intl'; import type { $npm$ReactIntl$IntlShape } from 'react-intl'; @@ -14,12 +14,20 @@ import globalMessages from '../../../../i18n/global-messages'; import type { PoolData } from '../../../../containers/wallet/staking/SeizaFetcher'; import RewardGraph from './RewardsGraph'; import type { GraphData } from '../dashboard/StakingDashboard'; +import { EpochProgressCard } from './EpochProgressCard'; +import moment from 'moment'; type Props = {| delegatedPool: PoolData, + epochProgress: {| + currentEpoch: number, + startEpochDate: string, + endEpochDate: string, + percentage: number, + |}, +undelegate: void | (void => Promise<void>), +epochLength: ?number, - +graphData: GraphData + +graphData: GraphData, |}; type Intl = {| @@ -46,9 +54,10 @@ function StakingTabs({ delegatedPool, epochLength, undelegate, + epochProgress, intl, - graphData - }: Props & Intl): Node { + graphData, +}: Props & Intl): Node { const [value, setValue] = useState(0); const handleChange = (event, newValue) => { @@ -63,9 +72,9 @@ function StakingTabs({ return epochLength === 1 ? intl.formatMessage(messages.singleEpochAxisLabel) : intl.formatMessage(messages.epochAxisLabel, { epochLength }); - } + }; - const { hideYAxis, items } = graphData.rewardsGraphData + const { hideYAxis, items } = graphData.rewardsGraphData; const tabs = [ { id: 0, @@ -99,8 +108,16 @@ function StakingTabs({ { id: 2, label: 'Epoch progress', - disabled: true, - component: <Box>TODO: Epoch progress!</Box>, + disabled: false, + component: ( + <EpochProgressCard + percentage={epochProgress.percentage} + days={moment(epochProgress.endEpochDate).diff(moment(), 'days')} + currentEpoch={epochProgress.currentEpoch} + startEpochDate={epochProgress.startEpochDate} + endEpochDate={epochProgress.endEpochDate} + /> + ), }, ]; diff --git a/packages/yoroi-extension/app/containers/profile/UriPromptPage.js b/packages/yoroi-extension/app/containers/profile/UriPromptPage.js index ee29484524..4af863dbed 100644 --- a/packages/yoroi-extension/app/containers/profile/UriPromptPage.js +++ b/packages/yoroi-extension/app/containers/profile/UriPromptPage.js @@ -21,6 +21,7 @@ import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import type { ServerStatusErrorType } from '../../types/serverStatusErrorType'; import { PublicDeriver } from '../../api/ada/lib/storage/models/PublicDeriver/index'; import { isTestnet } from '../../api/ada/lib/storage/database/prepackaged/networks'; +import { trackUriPrompt } from '../../api/analytics'; type GeneratedData = typeof UriPromptPage.prototype.generated; @@ -39,10 +40,12 @@ export default class UriPromptPage extends Component<InjectedOrGenerated<Generat runInAction(() => { this.isAccepted = true; }); + trackUriPrompt('allow'); }; onSkip: void => void = () => { this.generated.actions.profile.acceptUriScheme.trigger() + trackUriPrompt('skip'); }; onBack: void => void = () => { diff --git a/packages/yoroi-extension/app/containers/settings/categories/BlockchainSettingsPage.js b/packages/yoroi-extension/app/containers/settings/categories/BlockchainSettingsPage.js index 5d1ae8d5e9..fc2cdeae2b 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/BlockchainSettingsPage.js +++ b/packages/yoroi-extension/app/containers/settings/categories/BlockchainSettingsPage.js @@ -23,6 +23,7 @@ import { SelectedExplorer } from '../../../domain/SelectedExplorer'; import type { GetAllExplorersResponse, } from '../../../api/ada/lib/storage/bridge/explorers'; +import { trackUriPrompt } from '../../../api/analytics'; type GeneratedData = typeof BlockchainSettingsPage.prototype.generated; @@ -50,7 +51,12 @@ export default class BlockchainSettingsPage extends Component<InjectedOrGenerate ) ? ( <UriSettingsBlock - registerUriScheme={() => registerProtocols()} + registerUriScheme={ + () => { + registerProtocols(); + trackUriPrompt('allow'); + } + } isFirefox={environment.userAgentInfo.isFirefox()} /> ) diff --git a/packages/yoroi-extension/app/containers/settings/categories/GeneralSettingsPage.js b/packages/yoroi-extension/app/containers/settings/categories/GeneralSettingsPage.js index 5d2870cb2f..8f23f23343 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/GeneralSettingsPage.js +++ b/packages/yoroi-extension/app/containers/settings/categories/GeneralSettingsPage.js @@ -18,6 +18,7 @@ import { ReactComponent as AdaCurrency } from '../../../assets/images/currencie import { unitOfAccountDisabledValue } from '../../../types/unitOfAccountType'; import type { UnitOfAccountSettingType } from '../../../types/unitOfAccountType'; import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; +import { trackSetUnitOfAccount, trackSetLocale } from '../../../api/analytics'; const currencyLabels = defineMessages({ USD: { @@ -84,6 +85,12 @@ export default class GeneralSettingsPage extends Component<InjectedOrGenerated<G ? unitOfAccountDisabledValue : { enabled: true, currency: value }; await this.generated.actions.profile.updateUnitOfAccount.trigger(unitOfAccount); + trackSetUnitOfAccount(value); + }; + + onSelectLanguage: {| locale: string |} => PossiblyAsync<void> = ({ locale }) => { + this.generated.actions.profile.updateLocale.trigger({ locale }); + trackSetLocale(locale); }; render(): Node { @@ -120,7 +127,7 @@ export default class GeneralSettingsPage extends Component<InjectedOrGenerated<G return ( <> <GeneralSettings - onSelectLanguage={this.generated.actions.profile.updateLocale.trigger} + onSelectLanguage={this.onSelectLanguage} isSubmitting={isSubmittingLocale} languages={profileStore.LANGUAGE_OPTIONS} currentLocale={profileStore.currentLocale} diff --git a/packages/yoroi-extension/app/containers/settings/categories/RemoveWalletDialogContainer.js b/packages/yoroi-extension/app/containers/settings/categories/RemoveWalletDialogContainer.js index 60fa635b42..eed5c51322 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/RemoveWalletDialogContainer.js +++ b/packages/yoroi-extension/app/containers/settings/categories/RemoveWalletDialogContainer.js @@ -17,6 +17,7 @@ import { withLayout } from '../../../styles/context/layout'; import type { LayoutComponentMap } from '../../../styles/context/layout'; import { getWalletType } from '../../../stores/toplevel/WalletSettingsStore'; import type { WalletsNavigation } from '../../../api/localStorage' +import { trackRemoveWallet } from '../../../api/analytics'; export type GeneratedData = typeof RemoveWalletDialogContainer.prototype.generated; @@ -75,6 +76,7 @@ class RemoveWalletDialogContainer extends Component<AllProps> { quickAccess: walletsNavigation.quickAccess.filter(walletId => walletId !== selectedWalletId) } await this.generated.actions.profile.updateSortedWalletList.trigger(newWalletsNavigation); + trackRemoveWallet(); } this.props.publicDeriver && @@ -99,11 +101,14 @@ class RemoveWalletDialogContainer extends Component<AllProps> { onCancel={this.generated.actions.dialogs.closeActiveDialog.trigger} primaryButton={{ label: intl.formatMessage(globalMessages.remove), - onClick: () => - this.props.publicDeriver && - settingsActions.removeWallet.trigger({ - publicDeriver: this.props.publicDeriver, - }), + onClick: () => { + if (this.props.publicDeriver != null) { + settingsActions.removeWallet.trigger({ + publicDeriver: this.props.publicDeriver, + }); + trackRemoveWallet(); + } + } }} secondaryButton={{ onClick: this.generated.actions.dialogs.closeActiveDialog.trigger, diff --git a/packages/yoroi-extension/app/containers/settings/categories/ResyncWalletDialogContainer.js b/packages/yoroi-extension/app/containers/settings/categories/ResyncWalletDialogContainer.js index e41c482456..7a86838c4f 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/ResyncWalletDialogContainer.js +++ b/packages/yoroi-extension/app/containers/settings/categories/ResyncWalletDialogContainer.js @@ -13,6 +13,7 @@ import type { InjectedOrGenerated } from '../../../types/injectedPropsType'; import DangerousActionDialog from '../../../components/widgets/DangerousActionDialog'; import LocalizableError from '../../../i18n/LocalizableError'; +import { trackResyncWallet } from '../../../api/analytics'; export type GeneratedData = typeof ResyncWalletDialogContainer.prototype.generated; @@ -65,6 +66,7 @@ export default class ResyncWalletDialogContainer extends Component<Props> { publicDeriver: this.props.publicDeriver, }); this.generated.actions.dialogs.closeActiveDialog.trigger(); + trackResyncWallet(); } }} onCancel={this.generated.actions.dialogs.closeActiveDialog.trigger} diff --git a/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js b/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js index 45a3395701..49f4d087e2 100644 --- a/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js +++ b/packages/yoroi-extension/app/containers/settings/categories/WalletSettingsPage.js @@ -27,6 +27,7 @@ import type { SigningKeyCache } from '../../../stores/toplevel/WalletStore'; import LocalizableError from '../../../i18n/LocalizableError'; import type { RenameModelFunc } from '../../../api/common/index'; import type { IGetSigningKey } from '../../../api/ada/lib/storage/models/PublicDeriver/interfaces'; +import { trackExportWallet } from '../../../api/analytics'; type GeneratedData = typeof WalletSettingsPage.prototype.generated; @@ -108,9 +109,12 @@ export default class WalletSettingsPage extends Component<InjectedOrGenerated<Ge })} /> <ExportWallet - openDialog={() => actions.dialogs.open.trigger({ - dialog: ExportWalletDialogContainer, - })} + openDialog={() => { + actions.dialogs.open.trigger({ + dialog: ExportWalletDialogContainer, + }); + trackExportWallet(); + }} /> <RemoveWallet walletName={settingsCache.conceptualWalletName} diff --git a/packages/yoroi-extension/app/containers/transfer/WithdrawalTxDialogContainer.js b/packages/yoroi-extension/app/containers/transfer/WithdrawalTxDialogContainer.js index de6c1c4b87..48c240b274 100644 --- a/packages/yoroi-extension/app/containers/transfer/WithdrawalTxDialogContainer.js +++ b/packages/yoroi-extension/app/containers/transfer/WithdrawalTxDialogContainer.js @@ -17,6 +17,7 @@ import { } from '../../api/common/lib/MultiToken'; import type { NetworkRow, TokenRow } from '../../api/ada/lib/storage/database/primitives/tables'; import { getDefaultEntryToken } from '../../stores/toplevel/TokenInfoStore'; +import { trackWithdrawal } from '../../api/analytics'; export type GeneratedData = typeof WithdrawalTxDialogContainer.prototype.generated; @@ -34,7 +35,10 @@ export default class WithdrawalTxDialogContainer extends Component<Props> { render(): Node { const { intl } = this.context; - const { createWithdrawalTx } = this.generated.stores.substores.ada.delegationTransaction; + const { + createWithdrawalTx, + shouldDeregister, + } = this.generated.stores.substores.ada.delegationTransaction; if (this.generated.stores.profile.selectedNetwork == null) { throw new Error(`${nameof(WithdrawalTxDialogContainer)} no selected network`); @@ -51,7 +55,9 @@ export default class WithdrawalTxDialogContainer extends Component<Props> { label: intl.formatMessage(globalMessages.cancel), }} onSubmit={{ - trigger: () => {}, // nothing extra to do + trigger: () => { + trackWithdrawal(shouldDeregister); + }, label: intl.formatMessage(globalMessages.confirm), }} transactionRequest={createWithdrawalTx} @@ -97,6 +103,7 @@ export default class WithdrawalTxDialogContainer extends Component<Props> { error: ?LocalizableError, result: ?ISignRequest<any> |}, + shouldDeregister: boolean, |}, |}, |}, @@ -128,6 +135,7 @@ export default class WithdrawalTxDialogContainer extends Component<Props> { result: stores.substores.ada.delegationTransaction.createWithdrawalTx.result, reset: stores.substores.ada.delegationTransaction.createWithdrawalTx.reset, }, + shouldDeregister: stores.substores.ada.delegationTransaction.shouldDeregister, }, }, }, diff --git a/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.stories.js b/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.stories.js index 66313bc1ff..f8682fccdf 100644 --- a/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.stories.js +++ b/packages/yoroi-extension/app/containers/transfer/YoroiTransferPage.stories.js @@ -559,6 +559,7 @@ export const WithdrawalTxPage = (): Node => { ), reset: action('createWithdrawalTx reset'), }, + shouldDeregister: false, }, }, }, diff --git a/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js b/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js index aa5e5935b6..4c6c33e8bc 100644 --- a/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js +++ b/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js @@ -49,6 +49,7 @@ import { withLayout } from '../../styles/context/layout'; import WalletSendPreviewStepContainer from '../../components/wallet/send/WalletSendFormSteps/WalletSendPreviewStepContainer'; import AddNFTDialog from '../../components/wallet/send/WalletSendFormSteps/AddNFTDialog'; import AddTokenDialog from '../../components/wallet/send/WalletSendFormSteps/AddTokenDialog'; +import { trackSend } from '../../api/analytics'; const messages = defineMessages({ txConfirmationLedgerNanoLine1: { @@ -397,11 +398,14 @@ class WalletSendPage extends Component<AllProps> { isSubmitting={ledgerSendStore.isActionProcessing} error={ledgerSendStore.error} onSubmit={ - () => ledgerSendAction.sendUsingLedgerWallet.trigger({ - params: { signRequest }, - publicDeriver, - onSuccess: this.openTransactionSuccessDialog, - }) + () => { + ledgerSendAction.sendUsingLedgerWallet.trigger({ + params: { signRequest }, + publicDeriver, + onSuccess: this.openTransactionSuccessDialog, + }); + trackSend(); + } } onCancel={ledgerSendAction.cancel.trigger} unitOfAccountSetting={this.generated.stores.profile.unitOfAccount} @@ -431,11 +435,14 @@ class WalletSendPage extends Component<AllProps> { isSubmitting={trezorSendStore.isActionProcessing} error={trezorSendStore.error} onSubmit={ - () => trezorSendAction.sendUsingTrezor.trigger({ - params: { signRequest }, - publicDeriver, - onSuccess: this.openTransactionSuccessDialog, - }) + () => { + trezorSendAction.sendUsingTrezor.trigger({ + params: { signRequest }, + publicDeriver, + onSuccess: this.openTransactionSuccessDialog, + }) + trackSend(); + } } onCancel={trezorSendAction.cancel.trigger} unitOfAccountSetting={this.generated.stores.profile.unitOfAccount} diff --git a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js index bff0bf286b..e35eaa0534 100644 --- a/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js +++ b/packages/yoroi-extension/app/containers/wallet/dialogs/WalletSendConfirmationDialogContainer.js @@ -13,6 +13,7 @@ import { addressToDisplayString } from '../../../api/ada/lib/storage/bridge/util import type { ISignRequest } from '../../../api/common/lib/transactions/ISignRequest'; import type { TokenInfoMap } from '../../../stores/toplevel/TokenInfoStore'; import { genLookupOrFail } from '../../../stores/stateless/tokenHelpers'; +import { trackSend } from '../../../api/analytics'; export type GeneratedData = typeof WalletSendConfirmationDialogContainer.prototype.generated; @@ -85,6 +86,7 @@ export default class WalletSendConfirmationDialogContainer extends Component<Pro publicDeriver, onSuccess: openTransactionSuccessDialog, }); + trackSend(); }} isSubmitting={sendMoneyRequest.isExecuting} onCancel={() => { diff --git a/packages/yoroi-extension/app/containers/wallet/staking/CardanoStakingPage.js b/packages/yoroi-extension/app/containers/wallet/staking/CardanoStakingPage.js index 799f27f896..2f14391571 100644 --- a/packages/yoroi-extension/app/containers/wallet/staking/CardanoStakingPage.js +++ b/packages/yoroi-extension/app/containers/wallet/staking/CardanoStakingPage.js @@ -216,6 +216,9 @@ class CardanoStakingPage extends Component<AllProps, State> { const txRequests = this.generated.stores.transactions.getTxRequests(publicDeriver); const balance = txRequests.requests.getBalanceRequest.result; + if (balance == null) { + return null; + } const rewardBalance = delegationRequests.getDelegatedBalance.result == null ? new MultiToken([], publicDeriver.getParent().getDefaultToken()) @@ -223,8 +226,6 @@ class CardanoStakingPage extends Component<AllProps, State> { const tokenInfo = genLookupOrFail( this.generated.stores.tokenInfoStore.tokenInfo )(rewardBalance.getDefaultEntry()); - - if (balance == null || rewardBalance == null) throw new Error(`${nameof(CardanoStakingPage)} balance or rewardBalance is null`) return balance .joinAddCopy(rewardBalance) .getDefaultEntry() diff --git a/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.stories.js b/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.stories.js index 47d07bea8c..134b155f02 100644 --- a/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.stories.js +++ b/packages/yoroi-extension/app/containers/wallet/staking/StakingDashboardPage.stories.js @@ -1014,6 +1014,7 @@ export const AdaWithdrawDialog = (): Node => { ), reset: action('createWithdrawalTx reset'), }, + shouldDeregister: false, }, }, }, diff --git a/packages/yoroi-extension/app/containers/wallet/staking/StakingPage.js b/packages/yoroi-extension/app/containers/wallet/staking/StakingPage.js index 3b9a5402b1..24aad61761 100644 --- a/packages/yoroi-extension/app/containers/wallet/staking/StakingPage.js +++ b/packages/yoroi-extension/app/containers/wallet/staking/StakingPage.js @@ -51,9 +51,13 @@ import type { GeneratedData as DeregisterDialogContainerData } from '../../trans import UndelegateDialog from '../../../components/wallet/staking/dashboard/UndelegateDialog'; import type { PoolRequest } from '../../../api/jormungandr/lib/storage/bridge/delegationUtils'; import { generateGraphData } from '../../../utils/graph'; -import { ApiOptions, getApiForNetwork, } from '../../../api/common/utils'; -import type { CurrentTimeRequests, TimeCalcRequests } from '../../../stores/base/BaseCardanoTimeStore'; import type { TokenEntry } from '../../../api/common/lib/MultiToken'; +import { ApiOptions, getApiForNetwork } from '../../../api/common/utils'; +import type { + CurrentTimeRequests, + TimeCalcRequests, +} from '../../../stores/base/BaseCardanoTimeStore'; +import moment from 'moment'; export type GeneratedData = typeof StakingPage.prototype.generated; // populated by ConfigWebpackPlugin @@ -81,8 +85,9 @@ class StakingPage extends Component<AllProps> { if (!isCardanoHaskell(publicDeriver.getParent().getNetworkInfo())) { return undefined; } - const adaDelegationRequests = this.generated.stores.substores.ada.delegation - .getDelegationRequests(publicDeriver); + const adaDelegationRequests = this.generated.stores.substores.ada.delegation.getDelegationRequests( + publicDeriver + ); if (adaDelegationRequests == null) return undefined; return adaDelegationRequests.getRegistrationHistory.result?.current; }; @@ -168,6 +173,37 @@ class StakingPage extends Component<AllProps> { getStakePools: (PublicDeriver<>) => Node | void = publicDeriver => { + const timeStore = this.generated.stores.time; + const timeCalcRequests = timeStore.getTimeCalcRequests(publicDeriver); + const currTimeRequests = timeStore.getCurrentTimeRequests(publicDeriver); + const toAbsoluteSlot = timeCalcRequests.requests.toAbsoluteSlot.result; + if (toAbsoluteSlot == null) return undefined; + const toRealTime = timeCalcRequests.requests.toRealTime.result; + if (toRealTime == null) return undefined; + const timeSinceGenesis = timeCalcRequests.requests.timeSinceGenesis.result; + if (timeSinceGenesis == null) return undefined; + const getEpochLength = timeCalcRequests.requests.currentEpochLength.result; + if (getEpochLength == null) return undefined; + const currentEpoch = currTimeRequests.currentEpoch; + const epochLength = getEpochLength(); + + const getDateFromEpoch = epoch => { + const epochTime = toRealTime({ + absoluteSlotNum: toAbsoluteSlot({ + epoch, + // in Jormungandr, rewards were distributed at the start of the epoch + // in Haskell, rewards are calculated at the start of the epoch but distributed at the end + slot: isJormungandr(publicDeriver.getParent().getNetworkInfo()) ? 0 : getEpochLength(), + }), + timeSinceGenesisFunc: timeSinceGenesis, + }); + const epochMoment = moment(epochTime).format('lll'); + return epochMoment; + }; + + const endEpochDate = getDateFromEpoch(currentEpoch); + const previousEpochDate = getDateFromEpoch(currentEpoch - 1); + const delegationStore = this.generated.stores.delegation; const delegationRequests = delegationStore.getDelegationRequests(publicDeriver); if (delegationRequests == null) { @@ -212,20 +248,26 @@ class StakingPage extends Component<AllProps> { // tw: '', // }, // }; - return ( <StakingTabs + epochProgress={{ + startEpochDate: previousEpochDate, + currentEpoch, + endEpochDate, + percentage: Math.floor((100 * currTimeRequests.currentSlot) / epochLength), + }} delegatedPool={delegatedPool} undelegate={ // don't support undelegation for ratio stake since it's a less intuitive UX currentPools.length === 1 && isJormungandr(publicDeriver.getParent().getNetworkInfo()) ? async () => { this.generated.actions.dialogs.open.trigger({ dialog: UndelegateDialog }); - await this.generated.actions.jormungandr - .delegationTransaction.createTransaction.trigger({ + await this.generated.actions.jormungandr.delegationTransaction.createTransaction.trigger( + { publicDeriver, poolRequest: undefined, - }); + } + ); } : undefined } @@ -246,8 +288,8 @@ class StakingPage extends Component<AllProps> { toUnitOfAccount: TokenEntry => void | {| currency: string, amount: string |} = entry => { const { stores } = this.generated; const tokenRow = stores.tokenInfoStore.tokenInfo - .get(entry.networkId.toString()) - ?.get(entry.identifier); + .get(entry.networkId.toString()) + ?.get(entry.identifier); if (tokenRow == null) return undefined; if (!stores.profile.unitOfAccount.enabled) return undefined; @@ -362,10 +404,11 @@ class StakingPage extends Component<AllProps> { onNext={() => { // note: purposely don't await // since the next dialog will properly render the spinner - this.generated.actions.ada.delegationTransaction - .createWithdrawalTxForWallet.trigger({ - publicDeriver, - }); + this.generated.actions.ada.delegationTransaction.createWithdrawalTxForWallet.trigger( + { + publicDeriver, + } + ); this.generated.actions.dialogs.open.trigger({ dialog: WithdrawalTxDialogContainer, }); @@ -455,15 +498,15 @@ class StakingPage extends Component<AllProps> { |}, |}, |}, + time: {| + getCurrentTimeRequests: (PublicDeriver<>) => CurrentTimeRequests, + getTimeCalcRequests: (PublicDeriver<>) => TimeCalcRequests, + |}, delegation: {| selectedPage: number, getLocalPoolInfo: ($ReadOnly<NetworkRow>, string) => void | PoolMeta, getDelegationRequests: (PublicDeriver<>) => void | DelegationRequests, |}, - time: {| - getCurrentTimeRequests: (PublicDeriver<>) => CurrentTimeRequests, - getTimeCalcRequests: (PublicDeriver<>) => TimeCalcRequests, - |}, profile: {| shouldHideBalance: boolean, unitOfAccount: UnitOfAccountSettingType, @@ -507,10 +550,11 @@ class StakingPage extends Component<AllProps> { } return { getTimeCalcRequests: (undefined: any), - getCurrentTimeRequests: () => { throw new Error(`${nameof(StakingPage)} api not supported`) }, + getCurrentTimeRequests: () => { + throw new Error(`${nameof(StakingPage)} api not supported`); + }, }; })(); - return Object.freeze({ stores: { wallets: { @@ -525,6 +569,7 @@ class StakingPage extends Component<AllProps> { getLocalPoolInfo: stores.delegation.getLocalPoolInfo, getDelegationRequests: stores.delegation.getDelegationRequests, }, + time, uiDialogs: { isOpen: stores.uiDialogs.isOpen, getParam: stores.uiDialogs.getParam, @@ -540,7 +585,6 @@ class StakingPage extends Component<AllProps> { coinPriceStore: { getCurrentPrice: stores.coinPriceStore.getCurrentPrice, }, - time, substores: { ada: { delegation: { diff --git a/packages/yoroi-extension/app/routes-config.js b/packages/yoroi-extension/app/routes-config.js index ff4536e54d..2cbcef7d3b 100644 --- a/packages/yoroi-extension/app/routes-config.js +++ b/packages/yoroi-extension/app/routes-config.js @@ -1,4 +1,27 @@ // @flow +// routes to by tracked by analytics +export const TRACKED_ROUTES: RegExp = new RegExp( + '^(' + + '(/my-wallets)|' + + '(/wallets/add)|' + + '(/wallets/transactions)|' + + '(/wallets/send)|' + + '(/wallets/assets)|' + + '(/wallets/receive/.+)|' + + '(/wallets/delegation-dashboard)|' + + '(/wallets/cardano-delegation)|' + + '(/wallets/voting)|' + + '(/settings/.+)|' + + '(/transfer(/.+)?)|' + + '(/send-from-uri)|' + + '(/notice-board)|' + + '(/staking)|' + + '(/assets/.*)|' + + '(/connector/connected-websites)|' + + '(/experimental/.*)' + + ')$' +); + export const ROUTES = { ROOT: '/', diff --git a/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js b/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js index 3341357905..e03d287da3 100644 --- a/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js +++ b/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js @@ -27,6 +27,7 @@ import { import { MultiToken } from '../../api/common/lib/MultiToken'; import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; +import { trackDelegation } from '../../api/analytics'; export default class AdaDelegationTransactionStore extends Store<StoresMap, ActionsMap> { @@ -214,9 +215,7 @@ export default class AdaDelegationTransactionStore extends Store<StoresMap, Acti }, refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), }); - return; - } - if (isTrezorTWallet(request.publicDeriver.getParent())) { + } else if (isTrezorTWallet(request.publicDeriver.getParent())) { await this.stores.substores.ada.wallets.adaSendAndRefresh({ broadcastRequest: { trezor: { @@ -226,23 +225,23 @@ export default class AdaDelegationTransactionStore extends Store<StoresMap, Acti }, refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), }); - return; - } - - // normal password-based wallet - if (request.password == null) { - throw new Error(`${nameof(this._signTransaction)} missing password for non-hardware signing`); - } - await this.stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - normal: { - publicDeriver: request.publicDeriver, - password: request.password, - signRequest: result.signTxRequest, + } else { + // normal password-based wallet + if (request.password == null) { + throw new Error(`${nameof(this._signTransaction)} missing password for non-hardware signing`); + } + await this.stores.substores.ada.wallets.adaSendAndRefresh({ + broadcastRequest: { + normal: { + publicDeriver: request.publicDeriver, + password: request.password, + signRequest: result.signTxRequest, + }, }, - }, - refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), - }); + refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), + }); + } + trackDelegation(); } _complete: void => void = () => { diff --git a/packages/yoroi-extension/app/stores/ada/AdaWalletRestoreStore.js b/packages/yoroi-extension/app/stores/ada/AdaWalletRestoreStore.js index 017c230947..7737985e54 100644 --- a/packages/yoroi-extension/app/stores/ada/AdaWalletRestoreStore.js +++ b/packages/yoroi-extension/app/stores/ada/AdaWalletRestoreStore.js @@ -14,6 +14,7 @@ import type { } from '../../api/ada/lib/storage/models/PublicDeriver/interfaces'; import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; +import { trackWalletCreation } from '../../api/analytics'; export default class AdaWalletRestoreStore extends Store<StoresMap, ActionsMap> { @@ -129,6 +130,7 @@ export default class AdaWalletRestoreStore extends Store<StoresMap, ActionsMap> }); return wallet; }).promise; + trackWalletCreation('restored'); }; teardown(): void { diff --git a/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js b/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js index 821f8d4909..4dbc040ba4 100644 --- a/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js +++ b/packages/yoroi-extension/app/stores/ada/AdaWalletsStore.js @@ -12,6 +12,7 @@ import { buildCheckAndCall } from '../lib/check'; import { getApiForNetwork, ApiOptions } from '../../api/common/utils'; import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; +import { trackWalletCreation } from '../../api/analytics'; export default class AdaWalletsStore extends Store<StoresMap, ActionsMap> { @@ -129,5 +130,7 @@ export default class AdaWalletsStore extends Store<StoresMap, ActionsMap> { }); return wallet; }).promise; + + trackWalletCreation('created'); }; } diff --git a/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js b/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js index 96cdde4094..de52bce9f1 100644 --- a/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js +++ b/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js @@ -61,6 +61,7 @@ import type { StoresMap } from '../index'; import type { GetExtendedPublicKeyResponse, } from '@cardano-foundation/ledgerjs-hw-app-cardano'; +import { trackWalletCreation } from '../../api/analytics'; export default class LedgerConnectStore extends Store<StoresMap, ActionsMap> @@ -365,6 +366,7 @@ export default class LedgerConnectStore await this._saveHW( walletName, ); + trackWalletCreation('hardware'); }; /** creates new wallet and loads it */ diff --git a/packages/yoroi-extension/app/stores/ada/TrezorConnectStore.js b/packages/yoroi-extension/app/stores/ada/TrezorConnectStore.js index 23abfad637..1b6bb28333 100644 --- a/packages/yoroi-extension/app/stores/ada/TrezorConnectStore.js +++ b/packages/yoroi-extension/app/stores/ada/TrezorConnectStore.js @@ -51,6 +51,7 @@ import type { } from '../../actions/common/wallet-restore-actions'; import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; +import { trackWalletCreation } from '../../api/analytics'; type TrezorConnectionResponse = {| trezorResp: Success<CardanoPublicKey> | Unsuccessful, @@ -333,6 +334,7 @@ export default class TrezorConnectStore await this._saveHW( walletName, ); + trackWalletCreation('hardware'); }; /** creates new wallet and loads it */ diff --git a/packages/yoroi-extension/app/stores/ada/VotingStore.js b/packages/yoroi-extension/app/stores/ada/VotingStore.js index 4a7dd6bccd..36a4f2282a 100644 --- a/packages/yoroi-extension/app/stores/ada/VotingStore.js +++ b/packages/yoroi-extension/app/stores/ada/VotingStore.js @@ -45,6 +45,7 @@ import { generateRegistration } from '../../api/ada/lib/cardanoCrypto/catalyst'; import { derivePublicByAddressing } from '../../api/ada/lib/cardanoCrypto/utils' import type { ConceptualWallet } from '../../api/ada/lib/storage/models/ConceptualWallet' import type { CatalystRoundInfoResponse } from '../../api/ada/lib/state-fetch/types' +import { trackCatalystRegistration } from '../../api/analytics'; export const ProgressStep = Object.freeze({ GENERATE: 0, @@ -392,9 +393,7 @@ export default class VotingStore extends Store<StoresMap, ActionsMap> { }, refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), }); - return; - } - if (isTrezorTWallet(request.publicDeriver.getParent())) { + } else if (isTrezorTWallet(request.publicDeriver.getParent())) { await this.stores.substores.ada.wallets.adaSendAndRefresh({ broadcastRequest: { trezor: { @@ -404,23 +403,23 @@ export default class VotingStore extends Store<StoresMap, ActionsMap> { }, refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), }); - return; - } - - // normal password-based wallet - if (request.password == null) { - throw new Error(`${nameof(this._signTransaction)} missing password for non-hardware signing`); - } - await this.stores.substores.ada.wallets.adaSendAndRefresh({ - broadcastRequest: { - normal: { - publicDeriver: request.publicDeriver, - password: request.password, - signRequest: result, + } else { + // normal password-based wallet + if (request.password == null) { + throw new Error(`${nameof(this._signTransaction)} missing password for non-hardware signing`); + } + await this.stores.substores.ada.wallets.adaSendAndRefresh({ + broadcastRequest: { + normal: { + publicDeriver: request.publicDeriver, + password: request.password, + signRequest: result, + }, }, - }, - refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), - }); + refreshWallet: () => this.stores.wallets.refreshWalletFromRemote(request.publicDeriver), + }); + } + trackCatalystRegistration(); }; @action _generateCatalystKey: void => Promise<void> = async () => { diff --git a/packages/yoroi-extension/app/stores/base/BaseProfileStore.js b/packages/yoroi-extension/app/stores/base/BaseProfileStore.js index 87370108ff..7c31d6f988 100644 --- a/packages/yoroi-extension/app/stores/base/BaseProfileStore.js +++ b/packages/yoroi-extension/app/stores/base/BaseProfileStore.js @@ -15,6 +15,10 @@ import type { UnitOfAccountSettingType } from '../../types/unitOfAccountType'; import { SUPPORTED_CURRENCIES } from '../../config/unitOfAccount'; import type { ComplexityLevelType } from '../../types/complexityLevelType'; import BaseProfileActions from '../../actions/base/base-profile-actions'; +import { + trackSetLocale, + trackUpdateTheme +} from '../../api/analytics'; interface CoinPriceStore { refreshCurrentUnit: Request<void => Promise<void>> @@ -221,13 +225,15 @@ export default class BaseProfileStore _acceptLocale: void => Promise<void> = async () => { // commit in-memory language to storage - await this.setProfileLocaleRequest.execute( - this.inMemoryLanguage != null ? this.inMemoryLanguage : BaseProfileStore.getDefaultLocale() - ); + const locale = this.inMemoryLanguage != null ? + this.inMemoryLanguage : + BaseProfileStore.getDefaultLocale(); + await this.setProfileLocaleRequest.execute(locale); await this.getProfileLocaleRequest.execute(); // eagerly cache runInAction(() => { this.inMemoryLanguage = null; }); + trackSetLocale(locale); }; _updateMomentJsLocaleAfterLocaleChange: void => void = () => { @@ -312,6 +318,7 @@ export default class BaseProfileStore await this.getCustomThemeRequest.execute(); // eagerly cache await this.setThemeRequest.execute(theme); await this.getThemeRequest.execute(); // eagerly cache + trackUpdateTheme(theme); }; diff --git a/packages/yoroi-extension/app/stores/toplevel/TransactionBuilderStore.js b/packages/yoroi-extension/app/stores/toplevel/TransactionBuilderStore.js index aa7f10e335..0ae6ea2695 100644 --- a/packages/yoroi-extension/app/stores/toplevel/TransactionBuilderStore.js +++ b/packages/yoroi-extension/app/stores/toplevel/TransactionBuilderStore.js @@ -270,10 +270,10 @@ export default class TransactionBuilderStore extends Store<StoresMap, ActionsMap if (this.plannedTxInfoMap.length === 0) return false; for (const token of this.plannedTxInfoMap) { // we only care about the value in non-sendall case - if ( - !token.shouldSendAll && !token.amount - ) { - return false; + if (!token.shouldSendAll) { + if (token.amount == null || new BigNumber(token.amount).isLessThanOrEqualTo(0)) { + return false; + } } if (this.receiver == null) { return false; @@ -293,7 +293,7 @@ export default class TransactionBuilderStore extends Store<StoresMap, ActionsMap if (filteredTokens.length === 0) return String(1_000_000); const fullConfig = getCardanoHaskellBaseConfig(network); const squashedConfig = fullConfig.reduce((acc, next) => Object.assign(acc, next), {}); - const fakeAmount = new BigNumber('0'); // amount doesn't matter for calculating min UTXO amount + const fakeAmount = new BigNumber('1000000'); const fakeMultitoken = new MultiToken( [{ identifier: defaultToken.Identifier, diff --git a/packages/yoroi-extension/chrome/constants.js b/packages/yoroi-extension/chrome/constants.js index b4b57f36a5..49f80c2b52 100644 --- a/packages/yoroi-extension/chrome/constants.js +++ b/packages/yoroi-extension/chrome/constants.js @@ -54,6 +54,9 @@ export function genCSP(request: {| connectSrc.push('https://*.zdassets.com/') connectSrc.push('https://emurgohelpdesk.zendesk.com/') + // Analytics + connectSrc.push('https://analytics.emurgo-rnd.com/'); + // wasm-eval is needed to compile WebAssembly in the browser // note: wasm-eval is not standardized but empirically works in Firefox & Chrome https://github.com/w3c/webappsec-csp/pull/293 const evalSrc = "'wasm-eval'"; diff --git a/packages/yoroi-extension/chrome/extension/index.js b/packages/yoroi-extension/chrome/extension/index.js index 5d401703c3..02aa079632 100644 --- a/packages/yoroi-extension/chrome/extension/index.js +++ b/packages/yoroi-extension/chrome/extension/index.js @@ -15,6 +15,7 @@ import { addCloseListener, TabIdKeys } from '../../app/utils/tabManager'; import { Logger } from '../../app/utils/logging'; import { LazyLoadPromises } from '../../app/Routes'; import environment from '../../app/environment'; +import { trackStartup } from '../../app/api/analytics'; // run MobX in strict mode configure({ enforceActions: 'always' }); @@ -30,6 +31,7 @@ const initializeYoroi: void => Promise<void> = async () => { const hashHistory = createHashHistory(); const history = syncHistoryWithStore(hashHistory, router); const stores = createStores(api, actions, router); + await trackStartup(stores); Logger.debug(`[yoroi] stores created`); diff --git a/packages/yoroi-extension/features/step_definitions/common-steps.js b/packages/yoroi-extension/features/step_definitions/common-steps.js index 2f0999da7e..5553002f5a 100644 --- a/packages/yoroi-extension/features/step_definitions/common-steps.js +++ b/packages/yoroi-extension/features/step_definitions/common-steps.js @@ -53,6 +53,7 @@ import { saveButton } from '../pages/newWalletPages'; import { allowPubKeysAndSwitchToYoroi, switchToTrezorAndAllow } from './trezor-steps'; +import * as helpers from '../support/helpers/helpers'; const { promisify } = require('util'); const fs = require('fs'); @@ -157,6 +158,7 @@ After(async function (scenario) { } } await this.driver.quit(); + await helpers.sleep(500); }); export async function getPlates(customWorld: any): Promise<any> { @@ -400,7 +402,7 @@ async function acceptUriPrompt(world: any) { Given(/^I have opened the extension$/, async function () { this.webDriverLogger.info(`Step: I have opened the extension`); - await this.driver.get(this.getExtensionUrl()); + await this.get(this.getExtensionUrl()); const browserName = await this.getBrowser(); if (browserName === 'firefox') { await this.driver.manage().window().maximize(); diff --git a/packages/yoroi-extension/features/step_definitions/connector-steps.js b/packages/yoroi-extension/features/step_definitions/connector-steps.js index 2af733fc5d..2467a87577 100644 --- a/packages/yoroi-extension/features/step_definitions/connector-steps.js +++ b/packages/yoroi-extension/features/step_definitions/connector-steps.js @@ -43,7 +43,7 @@ const connectorPopUpIsDisplayed = async (customWorld: Object) => { Given(/^I have opened the mock dApp$/, async function () { this.webDriverLogger.info(`Step: I have opened the mock dApp`); - await this.driver.get(mockDAppUrl); + await this.get(mockDAppUrl); }); Then(/^I open the mock dApp tab$/, async function () { diff --git a/packages/yoroi-extension/features/support/helpers/helpers.js b/packages/yoroi-extension/features/support/helpers/helpers.js index 2ed793e547..9a2d9f8a29 100644 --- a/packages/yoroi-extension/features/support/helpers/helpers.js +++ b/packages/yoroi-extension/features/support/helpers/helpers.js @@ -66,4 +66,8 @@ export function getCircularReplacer(): Object { } return value; }; +} + +export const sleep = (milliseconds: number): Promise<any> => { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); } \ No newline at end of file diff --git a/packages/yoroi-extension/features/support/webdriver.js b/packages/yoroi-extension/features/support/webdriver.js index 25bba311bb..94076cf7af 100644 --- a/packages/yoroi-extension/features/support/webdriver.js +++ b/packages/yoroi-extension/features/support/webdriver.js @@ -11,6 +11,8 @@ import { getMethod, getLogDate } from './helpers/helpers'; import { WindowManager } from './windowManager'; import { MockDAppWebpage } from '../mock-dApp-webpage'; import { testRunsDataDir } from './helpers/common-constants'; +import { WebDriverError } from 'selenium-webdriver/lib/error'; +import * as helpers from './helpers/helpers'; const fs = require('fs'); const simpleNodeLogger = require('simple-node-logger'); @@ -178,6 +180,21 @@ function CustomWorld(cmdInput: WorldInput) { return `moz-extension://${firefoxExtensionId}/main_window.html`; }; + this.get = async (url: string) => { + for (let i = 0; i < 6; i++) { + try { + await this.driver.get(url); + } catch (e) { + if (e instanceof WebDriverError) { + this.webDriverLogger.info(`Webdriver: Caught the WebDriverError. Sleep for 1 second and retry`); + await helpers.sleep(500); + continue; + } + } + break; + } + }; + this.getElementBy = (locator: LocatorObject) => this.driver.findElement(getMethod(locator.method)(locator.locator)); diff --git a/packages/yoroi-extension/package-lock.json b/packages/yoroi-extension/package-lock.json index dc6f236eef..428498a497 100644 --- a/packages/yoroi-extension/package-lock.json +++ b/packages/yoroi-extension/package-lock.json @@ -1,6 +1,6 @@ { "name": "yoroi", - "version": "4.15.200", + "version": "4.16.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1673,14 +1673,14 @@ "dev": true }, "@emurgo/cardano-serialization-lib-browser": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-browser/-/cardano-serialization-lib-browser-10.2.0.tgz", - "integrity": "sha512-b4RrWtC5y8+bjKp9sFo8IWXg+xIatXoUq4YcqFDReDN6dpHRjLlOJv7h9Da6VA5GXvLQy6vOFuieAZJFGng9FQ==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-browser/-/cardano-serialization-lib-browser-11.0.0.tgz", + "integrity": "sha512-aJHJSUdlXGu1wfr7sETdoE6mv43jdtflW03Jwro3kFQwchLGAOUiA4LfTDOKwgrtxT6lq5fQW46YoHh14PZDyg==" }, "@emurgo/cardano-serialization-lib-nodejs": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-nodejs/-/cardano-serialization-lib-nodejs-10.2.0.tgz", - "integrity": "sha512-rRWBQcbQlMj4GS7gt6toxRzY9cjMfFBWYKWrfH+eEqUXSO+3blKKA3T/yra3khxU/8+EAY1T94uoUDvjkrpTzg==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-nodejs/-/cardano-serialization-lib-nodejs-11.0.0.tgz", + "integrity": "sha512-thm7g+NT9W4Iwor0S7oTewpLoKwGTGVdVWiZsUj3GjNNp+XJQHXffsOy9JyauMHjqaQeIpDLR1QckY5VnCWzhg==", "dev": true }, "@emurgo/cip14-js": { diff --git a/packages/yoroi-extension/package.json b/packages/yoroi-extension/package.json index 894ecb0a8c..2137d2cd81 100644 --- a/packages/yoroi-extension/package.json +++ b/packages/yoroi-extension/package.json @@ -1,6 +1,6 @@ { "name": "yoroi", - "version": "4.15.200", + "version": "4.16.0", "description": "Cardano ADA wallet", "scripts": { "dev:build": "rimraf dev/ && babel-node scripts/build --type=debug", @@ -89,7 +89,7 @@ "@babel/runtime": "7.12.18", "@babel/runtime-corejs3": "7.12.18", "@emurgo/cardano-message-signing-nodejs": "1.0.1", - "@emurgo/cardano-serialization-lib-nodejs": "10.2.0", + "@emurgo/cardano-serialization-lib-nodejs": "11.0.0", "@emurgo/js-chain-libs-node": "0.7.1", "@pmmmwh/react-refresh-webpack-plugin": "0.4.3", "@storybook/addon-actions": "6.2.7", @@ -179,7 +179,7 @@ "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "@emurgo/cardano-message-signing-browser": "1.0.1", - "@emurgo/cardano-serialization-lib-browser": "10.2.0", + "@emurgo/cardano-serialization-lib-browser": "11.0.0", "@emurgo/cip14-js": "2.0.0", "@emurgo/cip4-js": "1.0.5", "@emurgo/js-chain-libs": "0.7.1",