diff --git a/.github/scripts/get-next-semver-version.sh b/.github/scripts/get-next-semver-version.sh
index 552e1fa7061..9af9b2d163d 100755
--- a/.github/scripts/get-next-semver-version.sh
+++ b/.github/scripts/get-next-semver-version.sh
@@ -21,7 +21,7 @@ VERSION_PACKAGE=$(node -p "require('../../package.json').version")
# Compare versions and keep the highest one
HIGHEST_VERSION=$(printf "%s\n%s\n%s" "$VERSION_BRANCHES" "$VERSION_TAGS" "$VERSION_PACKAGE" | sort --version-sort | tail -n 1)
-# Increment the minor version of the highest version found
-NEXT_VERSION=$(echo "$HIGHEST_VERSION" | awk -F. -v OFS=. '{$2++; print}')
+# Increment the minor version of the highest version found and reset the patch version to 0
+NEXT_VERSION=$(echo "$HIGHEST_VERSION" | awk -F. -v OFS=. '{$2++; $3=0; print}')
echo "NEXT_SEMVER_VERSION=${NEXT_VERSION}" >> "$GITHUB_ENV"
diff --git a/.nvmrc b/.nvmrc
index 3516580bbbc..2a393af592b 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-20.17.0
+20.18.0
diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js
index c08c085d9a9..bdf663ae293 100644
--- a/.storybook/storybook.requires.js
+++ b/.storybook/storybook.requires.js
@@ -126,6 +126,7 @@ const getStories = () => {
"./app/components/Views/confirmations/components/UI/InfoRow/InfoRow.stories.tsx": require("../app/components/Views/confirmations/components/UI/InfoRow/InfoRow.stories.tsx"),
"./app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.stories.tsx": require("../app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.stories.tsx"),
"./app/components/Views/confirmations/components/UI/Tooltip/Tooltip.stories.tsx": require("../app/components/Views/confirmations/components/UI/Tooltip/Tooltip.stories.tsx"),
+ "./app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.stories.tsx": require("../app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.stories.tsx"),
"./app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx": require("../app/component-library/components/Texts/SensitiveText/SensitiveText.stories.tsx"),
};
};
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0cfee4c59bf..8a753df77c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,77 @@
## Current Main Branch
+## 7.38.0 - Jan 16, 2024
+### Added
+- [#12427](https://github.com/MetaMask/metamask-mobile/pull/12427): feat: implement remote feature flag controller (#12427)
+- [#12507](https://github.com/MetaMask/metamask-mobile/pull/12507): feat: activate portfolio view (#12507)
+- [#12540](https://github.com/MetaMask/metamask-mobile/pull/12540): feat: migrate Base network RPC from https://mainnet.base.org to base-… (#12540)
+- [#12505](https://github.com/MetaMask/metamask-mobile/pull/12505): feat: add aggregated portfolio balance cross chains (#12505)
+- [#12417](https://github.com/MetaMask/metamask-mobile/pull/12417): feat: multichain detect tokens feat (#12417)
+- [#12419](https://github.com/MetaMask/metamask-mobile/pull/12419): feat: upgrade transaction controller to get incoming transactions using accounts API (#12419)
+- [#12537](https://github.com/MetaMask/metamask-mobile/pull/12537): feat: enable ledger clear signing feature (#12537)
+- [#12622](https://github.com/MetaMask/metamask-mobile/pull/12622): feat: Hide the smart transaction status page if we return a txHash asap (#12622)
+- [#12244](https://github.com/MetaMask/metamask-mobile/pull/12244): feat(ci): Expo (#12244)
+- [#12459](https://github.com/MetaMask/metamask-mobile/pull/12459): feat: upgrade profile-sync-controller to 1.0.0 (#12459)
+- [#12294](https://github.com/MetaMask/metamask-mobile/pull/12294): feat: Add Bitcoin accounts (Flask Only) (#12294)
+- [#12243](https://github.com/MetaMask/metamask-mobile/pull/12243): feat: cicd e2e label requirements + pr automation (#12243)
+- [#12495](https://github.com/MetaMask/metamask-mobile/pull/12495): feat: Support gas fee flows in swaps (#12495)
+- [#12431](https://github.com/MetaMask/metamask-mobile/pull/12431): feat: multi chain asset list (#12431)
+
+### Changed
+- [#12623](https://github.com/MetaMask/metamask-mobile/pull/12623): chore: update bug template to include feature branches (#12623)
+- [#12538](https://github.com/MetaMask/metamask-mobile/pull/12538): chore: Chore/12435 mvp handle engine does not exist (#12538)
+- [#12617](https://github.com/MetaMask/metamask-mobile/pull/12617): docs: Update README.md with new expo instructions (#12617)
+- [#12559](https://github.com/MetaMask/metamask-mobile/pull/12559): test: move remaining modal pages and selectors to their respective folders (#12559)
+- [#12556](https://github.com/MetaMask/metamask-mobile/pull/12556): test: remove redundent tests in quarantine folder (#12556)
+- [#12558](https://github.com/MetaMask/metamask-mobile/pull/12558): test: Create e2e tag for multi chain (#12558)
+- [#12531](https://github.com/MetaMask/metamask-mobile/pull/12531): test: Move files to Wallet folder (#12531)
+- [#12511](https://github.com/MetaMask/metamask-mobile/pull/12511): test: Move files to Onboarding folder (#12511)
+- [#12512](https://github.com/MetaMask/metamask-mobile/pull/12512): test: address regression pipeline slow down (#12512)
+- [#12513](https://github.com/MetaMask/metamask-mobile/pull/12513): ci: disable security e2e tests (#12513)
+- [#12602](https://github.com/MetaMask/metamask-mobile/pull/12602): chore: Additional e2e test to support `PortfolioView` (#12602)
+- [#12321](https://github.com/MetaMask/metamask-mobile/pull/12321): refactor: remove global network from transaction controller (#12321)
+- [#12536](https://github.com/MetaMask/metamask-mobile/pull/12536): test: fix mock server (#12536)
+- [#12288](https://github.com/MetaMask/metamask-mobile/pull/12288): test: add e2e test for security alert api (#12288)
+- [#12597](https://github.com/MetaMask/metamask-mobile/pull/12597): test(3615): additional e2e scenarios editing permissions and non permitted networks (#12597)
+- [#12488](https://github.com/MetaMask/metamask-mobile/pull/12488): test(3615): add new e2e test for initial dapp connection and non permitted flow (#12488)
+- [#12532](https://github.com/MetaMask/metamask-mobile/pull/12532): refactor: de-anonymize insensitive properties of swaps events (#12532)
+- [#12485](https://github.com/MetaMask/metamask-mobile/pull/12485): chore: Stop suppressing pod install failures (#12485)
+- [#12574](https://github.com/MetaMask/metamask-mobile/pull/12574): chore: Add option to skip pod install setup step (#12574)
+- [#12609](https://github.com/MetaMask/metamask-mobile/pull/12609): chore: update user storage E2E framework (#12609)
+- [#12569](https://github.com/MetaMask/metamask-mobile/pull/12569): chore: transfer ownership of auth & profile sync E2E from notifications to identity (#12569)
+- [#12534](https://github.com/MetaMask/metamask-mobile/pull/12534): chore: change ownership of profile sync from notifications to identity (#12534)
+- [#12543](https://github.com/MetaMask/metamask-mobile/pull/12543): chore: Decrease hot and cold start app to wallet view time (#12543)
+- [#12428](https://github.com/MetaMask/metamask-mobile/pull/12428): chore: Add eth hd keyring and key tree to decrease unlock time (#12428)
+- [#12555](https://github.com/MetaMask/metamask-mobile/pull/12555): chore: Update accounts packages (#12555)
+- [#12563](https://github.com/MetaMask/metamask-mobile/pull/12563): chore: cicd e2e hardening (#12563)
+- [#12554](https://github.com/MetaMask/metamask-mobile/pull/12554): chore: fail status when on no labels for retro-label changes (#12554)
+- [#12295](https://github.com/MetaMask/metamask-mobile/pull/12295): chore: use getShares contract method from stake-sdk for unstake all flow (#12295)
+- [#12551](https://github.com/MetaMask/metamask-mobile/pull/12551): chore: Bump Snaps packages (#12551)
+
+### Fixed
+- [#12650](https://github.com/MetaMask/metamask-mobile/pull/12650): fix: fix swaps button on asset overview page for multichain feature (#12650)
+- [#12659](https://github.com/MetaMask/metamask-mobile/pull/12659): fix: fix token details navigation (#12659)
+- [#12624](https://github.com/MetaMask/metamask-mobile/pull/12624): fix: add new translations (#12624)
+- [#12373](https://github.com/MetaMask/metamask-mobile/pull/12373): fix: circular dependencies engine-network-handleNetworkSwitch (#12373)
+- [#12663](https://github.com/MetaMask/metamask-mobile/pull/12663): fix: disable flaky tests on incoming-transactions.spec (#12663)
+- [#12598](https://github.com/MetaMask/metamask-mobile/pull/12598): fix: disable mock poc test (#12598)
+- [#12230](https://github.com/MetaMask/metamask-mobile/pull/12230): fix: Jest timer error in unit test (#12230)
+- [#12626](https://github.com/MetaMask/metamask-mobile/pull/12626): fix: fix flaky test (#12626)
+- [#12372](https://github.com/MetaMask/metamask-mobile/pull/12372): fix: abstract out circular dependencies between engine and networks util (#12372)
+- [#12641](https://github.com/MetaMask/metamask-mobile/pull/12641): fix: fix network selector (#12641)
+- [#12637](https://github.com/MetaMask/metamask-mobile/pull/12637): fix: fix native tokens filter when all networks is selected (#12637)
+- [#12529](https://github.com/MetaMask/metamask-mobile/pull/12529): fix: fix NFTs disappearing after killing app (#12529)
+- [#12562](https://github.com/MetaMask/metamask-mobile/pull/12562): fix: Move `AssetPollingProvider` from Root to Nav/Main/index.js (#12562)
+- [#12607](https://github.com/MetaMask/metamask-mobile/pull/12607): fix: e2e regression gas api (#12607)
+- [#12460](https://github.com/MetaMask/metamask-mobile/pull/12460): fix: add source when local PPOM fails (#12460)
+- [#12199](https://github.com/MetaMask/metamask-mobile/pull/12199): fix: 10967 User able to add Ledger account with existing account name (#12199)
+- [#12566](https://github.com/MetaMask/metamask-mobile/pull/12566): fix(12527): sdk connection with unknown url causes a bug (#12566)
+- [#12405](https://github.com/MetaMask/metamask-mobile/pull/12405): fix(431-2): active network icon has too much margin and adding optional prop (#12405)
+- [#12591](https://github.com/MetaMask/metamask-mobile/pull/12591): fix: add resolution for express to fix failing audit on path-to-regexp (#12591)
+- [#12567](https://github.com/MetaMask/metamask-mobile/pull/12567): fix: update input handling in useInputHandler to support BACK key functionality (#12567)
+- [#12630](https://github.com/MetaMask/metamask-mobile/pull/12630): fix: hide tokens without balance for multichain (#12630)
+
## 7.37.1 - Dec 16, 2024
### Fixed
- [#12577](https://github.com/MetaMask/metamask-mobile/pull/12577): chore: bump {gas-fee,network,selected-network,notification-services,profile-sync,signature}-controller (#12577)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 7c37abf3371..d934382fa02 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -178,8 +178,8 @@ android {
applicationId "io.metamask"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionName "7.37.1"
- versionCode 1534
+ versionName "7.38.0"
+ versionCode 1528
testBuildType System.getProperty('testBuildType', 'debug')
missingDimensionStrategy 'react-native-camera', 'general'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/app/actions/multichain/state.ts b/app/actions/multichain/state.ts
index 643638e0e91..a2015e89b59 100644
--- a/app/actions/multichain/state.ts
+++ b/app/actions/multichain/state.ts
@@ -1,4 +1,7 @@
+///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
export interface MultichainSettingsState {
bitcoinSupportEnabled: boolean;
bitcoinTestnetSupportEnabled: boolean;
+ solanaSupportEnabled: boolean;
}
+///: END:ONLY_INCLUDE_IF
diff --git a/app/component-library/base-components/TagBase/__snapshots__/TagBase.test.tsx.snap b/app/component-library/base-components/TagBase/__snapshots__/TagBase.test.tsx.snap
index a3be1a262ef..a3df343ffb5 100644
--- a/app/component-library/base-components/TagBase/__snapshots__/TagBase.test.tsx.snap
+++ b/app/component-library/base-components/TagBase/__snapshots__/TagBase.test.tsx.snap
@@ -9,7 +9,7 @@ exports[`TagBase should render TagBase 1`] = `
{
"alignSelf": "flex-start",
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 999,
"borderWidth": 0,
"color": "#141618",
diff --git a/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap b/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap
index f0bc6875e36..036368b43bd 100644
--- a/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap
+++ b/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap
@@ -284,7 +284,7 @@ exports[`CellSelectWithMenu should render with default settings correctly 1`] =
style={
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 10,
"borderWidth": 1,
"height": 24,
diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/__snapshots__/BadgeNetwork.test.tsx.snap b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/__snapshots__/BadgeNetwork.test.tsx.snap
index aed137096c5..726b40dac34 100644
--- a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/__snapshots__/BadgeNetwork.test.tsx.snap
+++ b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/__snapshots__/BadgeNetwork.test.tsx.snap
@@ -27,7 +27,7 @@ exports[`BadgeNetwork should render BadgeNetwork 1`] = `
"height": 32,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
diff --git a/app/component-library/components/Banners/Banner/__snapshots__/Banner.test.tsx.snap b/app/component-library/components/Banners/Banner/__snapshots__/Banner.test.tsx.snap
index 39786758805..7e99d8d276c 100644
--- a/app/component-library/components/Banners/Banner/__snapshots__/Banner.test.tsx.snap
+++ b/app/component-library/components/Banners/Banner/__snapshots__/Banner.test.tsx.snap
@@ -4,7 +4,7 @@ exports[`Banner should render correctly 1`] = `
= ({
- style,
- size = DEFAULT_TEXTFIELD_SIZE,
- startAccessory,
- endAccessory,
- isError = false,
- inputElement,
- isDisabled = false,
- autoFocus = false,
- onBlur,
- onFocus,
- ...props
-}) => {
+const TextField = React.forwardRef((
+ {
+ style,
+ size = DEFAULT_TEXTFIELD_SIZE,
+ startAccessory,
+ endAccessory,
+ isError = false,
+ inputElement,
+ isDisabled = false,
+ autoFocus = false,
+ onBlur,
+ onFocus,
+ ...props
+ },
+ ref
+) => {
const [isFocused, setIsFocused] = useState(autoFocus);
const { styles } = useStyles(styleSheet, {
@@ -87,6 +90,7 @@ const TextField: React.FC = ({
onBlur={onBlurHandler}
onFocus={onFocusHandler}
{...props}
+ ref={ref}
isStateStylesDisabled
/>
)}
@@ -101,6 +105,6 @@ const TextField: React.FC = ({
)}
);
-};
+});
export default TextField;
diff --git a/app/component-library/components/Form/TextField/__snapshots__/TextField.test.tsx.snap b/app/component-library/components/Form/TextField/__snapshots__/TextField.test.tsx.snap
index 96b84e5d5da..dfe8245cb0b 100644
--- a/app/component-library/components/Form/TextField/__snapshots__/TextField.test.tsx.snap
+++ b/app/component-library/components/Form/TextField/__snapshots__/TextField.test.tsx.snap
@@ -6,7 +6,7 @@ exports[`TextField should render default settings correctly 1`] = `
{
"alignItems": "center",
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"flexDirection": "row",
@@ -24,7 +24,7 @@ exports[`TextField should render default settings correctly 1`] = `
}
}
>
- = ({
+const Input = React.forwardRef(({
style,
textVariant = DEFAULT_TEXT_VARIANT,
isStateStylesDisabled,
@@ -23,7 +23,7 @@ const Input: React.FC = ({
onFocus,
autoFocus = true,
...props
-}) => {
+}, ref) => {
const [isFocused, setIsFocused] = useState(autoFocus);
const { styles } = useStyles(styleSheet, {
@@ -67,8 +67,9 @@ const Input: React.FC = ({
autoFocus={autoFocus}
onBlur={onBlurHandler}
onFocus={onFocusHandler}
+ ref={ref}
/>
);
-};
+});
export default Input;
diff --git a/app/component-library/components/Form/TextFieldSearch/__snapshots__/TextFieldSearch.test.tsx.snap b/app/component-library/components/Form/TextFieldSearch/__snapshots__/TextFieldSearch.test.tsx.snap
index af34f91633b..130ad728b8d 100644
--- a/app/component-library/components/Form/TextFieldSearch/__snapshots__/TextFieldSearch.test.tsx.snap
+++ b/app/component-library/components/Form/TextFieldSearch/__snapshots__/TextFieldSearch.test.tsx.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TextFieldSearch should render default settings correctly 1`] = `
- (
component={AddAsset}
options={AddAsset.navigationOptions}
/>
-
StyleSheet.create({
diff --git a/app/components/UI/AccountFromToInfoCard/__snapshots__/AccountFromToInfoCard.test.tsx.snap b/app/components/UI/AccountFromToInfoCard/__snapshots__/AccountFromToInfoCard.test.tsx.snap
index c7913411afa..e4c4e3c70e4 100644
--- a/app/components/UI/AccountFromToInfoCard/__snapshots__/AccountFromToInfoCard.test.tsx.snap
+++ b/app/components/UI/AccountFromToInfoCard/__snapshots__/AccountFromToInfoCard.test.tsx.snap
@@ -39,7 +39,7 @@ exports[`AccountFromToInfoCard should match snapshot 1`] = `
style={
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 4,
"borderWidth": 1,
"flexDirection": "row",
@@ -269,7 +269,7 @@ exports[`AccountFromToInfoCard should match snapshot 1`] = `
"height": 32,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -433,7 +433,7 @@ exports[`AccountFromToInfoCard should match snapshot 1`] = `
"paddingHorizontal": 10,
},
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
},
]
}
diff --git a/app/components/UI/AccountInfoCard/__snapshots__/index.test.tsx.snap b/app/components/UI/AccountInfoCard/__snapshots__/index.test.tsx.snap
index a1cbf235331..707e9f509e6 100644
--- a/app/components/UI/AccountInfoCard/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/AccountInfoCard/__snapshots__/index.test.tsx.snap
@@ -5,7 +5,7 @@ exports[`AccountInfoCard should match snapshot 1`] = `
style={
{
"alignItems": "center",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 10,
"borderWidth": 1,
"flexDirection": "row",
diff --git a/app/components/UI/AccountOverview/__snapshots__/index.test.tsx.snap b/app/components/UI/AccountOverview/__snapshots__/index.test.tsx.snap
index 75060e240a8..603b0d0074f 100644
--- a/app/components/UI/AccountOverview/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/AccountOverview/__snapshots__/index.test.tsx.snap
@@ -160,7 +160,7 @@ exports[`AccountOverview should render correctly 1`] = `
onPress={[Function]}
style={
{
- "backgroundColor": "#0376c91a",
+ "backgroundColor": "#0376C91A",
"borderRadius": 40,
"marginBottom": 20,
"marginTop": 20,
diff --git a/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap b/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap
index c798c9e9f88..b80c4f54dc7 100644
--- a/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/AccountRightButton/__snapshots__/index.test.tsx.snap
@@ -524,7 +524,7 @@ exports[`AccountRightButton should render correctly 1`] = `
"height": 32,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
diff --git a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap
index dd4954812d8..c4498419f07 100644
--- a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap
+++ b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap
@@ -61,7 +61,7 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = `
style={
{
"alignItems": "center",
- "backgroundColor": "#0376c91a",
+ "backgroundColor": "#0376C91A",
"flexDirection": "row",
}
}
@@ -385,7 +385,7 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = `
accessible={true}
style={
{
- "backgroundColor": "#0376c91a",
+ "backgroundColor": "#0376C91A",
"bottom": 0,
"flexDirection": "row",
"left": 0,
@@ -1617,7 +1617,7 @@ exports[`AccountSelectorList renders correctly 1`] = `
style={
{
"alignItems": "center",
- "backgroundColor": "#0376c91a",
+ "backgroundColor": "#0376C91A",
"flexDirection": "row",
}
}
@@ -1941,7 +1941,7 @@ exports[`AccountSelectorList renders correctly 1`] = `
accessible={true}
style={
{
- "backgroundColor": "#0376c91a",
+ "backgroundColor": "#0376C91A",
"bottom": 0,
"flexDirection": "row",
"left": 0,
@@ -2442,7 +2442,7 @@ exports[`AccountSelectorList should render all accounts but only the balance for
style={
{
"alignItems": "center",
- "backgroundColor": "#0376c91a",
+ "backgroundColor": "#0376C91A",
"flexDirection": "row",
}
}
@@ -2650,7 +2650,7 @@ exports[`AccountSelectorList should render all accounts but only the balance for
accessible={true}
style={
{
- "backgroundColor": "#0376c91a",
+ "backgroundColor": "#0376C91A",
"bottom": 0,
"flexDirection": "row",
"left": 0,
diff --git a/app/components/UI/AddCustomToken/__snapshots__/index.test.tsx.snap b/app/components/UI/AddCustomToken/__snapshots__/index.test.tsx.snap
index d2af139ec0f..81ef0037a49 100644
--- a/app/components/UI/AddCustomToken/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/AddCustomToken/__snapshots__/index.test.tsx.snap
@@ -23,7 +23,7 @@ exports[`AddCustomToken render matches previous snapshot 1`] = `
},
undefined,
{
- "backgroundColor": "#0376c91a",
+ "backgroundColor": "#0376C91A",
"borderColor": "#0376c9",
},
{
@@ -163,7 +163,7 @@ exports[`AddCustomToken render matches previous snapshot 1`] = `
returnKeyType="next"
style={
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"color": "#141618",
diff --git a/app/components/UI/AddressInputs/__snapshots__/index.test.jsx.snap b/app/components/UI/AddressInputs/__snapshots__/index.test.jsx.snap
index be35d0b77fb..9cdcbdd7409 100644
--- a/app/components/UI/AddressInputs/__snapshots__/index.test.jsx.snap
+++ b/app/components/UI/AddressInputs/__snapshots__/index.test.jsx.snap
@@ -39,7 +39,7 @@ exports[`AddressInputs AddressFrom should match default snapshot 1`] = `
style={
[
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"flex": 1,
@@ -50,7 +50,7 @@ exports[`AddressInputs AddressFrom should match default snapshot 1`] = `
"padding": 10,
},
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
},
]
}
@@ -220,7 +220,7 @@ exports[`AddressInputs AddressFrom should match snapshot when layout is vertical
style={
[
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"flex": 1,
@@ -231,7 +231,7 @@ exports[`AddressInputs AddressFrom should match snapshot when layout is vertical
"padding": 10,
},
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
},
]
}
@@ -413,7 +413,7 @@ exports[`AddressInputs AddressTo should match default snapshot 1`] = `
"paddingHorizontal": 10,
},
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
},
]
}
@@ -684,7 +684,7 @@ exports[`AddressInputs AddressTo should match snapshot when layout is vertical 1
"paddingHorizontal": 10,
},
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
},
]
}
diff --git a/app/components/UI/ApprovalTagUrl/__snapshots__/ApprovalTagUrl.test.tsx.snap b/app/components/UI/ApprovalTagUrl/__snapshots__/ApprovalTagUrl.test.tsx.snap
index b68dc550e4d..e5554cae4b3 100644
--- a/app/components/UI/ApprovalTagUrl/__snapshots__/ApprovalTagUrl.test.tsx.snap
+++ b/app/components/UI/ApprovalTagUrl/__snapshots__/ApprovalTagUrl.test.tsx.snap
@@ -7,7 +7,7 @@ exports[`ApprovalTagUrl renders correctly 1`] = `
"alignItems": "center",
"alignSelf": "center",
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 99,
"borderWidth": 1,
"flexDirection": "row",
diff --git a/app/components/UI/AssetOverview/AssetOverview.test.tsx b/app/components/UI/AssetOverview/AssetOverview.test.tsx
index 8be0735dace..2afe8c93a52 100644
--- a/app/components/UI/AssetOverview/AssetOverview.test.tsx
+++ b/app/components/UI/AssetOverview/AssetOverview.test.tsx
@@ -128,7 +128,12 @@ describe('AssetOverview', () => {
it('should render correctly', async () => {
const container = renderWithProvider(
- ,
+ ,
{ state: mockInitialState },
);
expect(container).toMatchSnapshot();
@@ -138,7 +143,12 @@ describe('AssetOverview', () => {
jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(true);
const container = renderWithProvider(
- ,
+ ,
{ state: mockInitialState },
);
expect(container).toMatchSnapshot();
@@ -146,7 +156,12 @@ describe('AssetOverview', () => {
it('should handle buy button press', async () => {
const { getByTestId } = renderWithProvider(
- ,
+ ,
{ state: mockInitialState },
);
@@ -163,7 +178,12 @@ describe('AssetOverview', () => {
it('should handle send button press', async () => {
const { getByTestId } = renderWithProvider(
- ,
+ ,
{ state: mockInitialState },
);
@@ -175,7 +195,12 @@ describe('AssetOverview', () => {
it('should handle swap button press', async () => {
const { getByTestId } = renderWithProvider(
- ,
+ ,
{ state: mockInitialState },
);
@@ -232,6 +257,7 @@ describe('AssetOverview', () => {
asset={asset}
displayBuyButton={false}
displaySwapsButton
+ swapsIsLive
/>,
{ state: mockInitialState },
);
diff --git a/app/components/UI/AssetOverview/AssetOverview.tsx b/app/components/UI/AssetOverview/AssetOverview.tsx
index c7270e4af75..d098316e0fa 100644
--- a/app/components/UI/AssetOverview/AssetOverview.tsx
+++ b/app/components/UI/AssetOverview/AssetOverview.tsx
@@ -74,12 +74,14 @@ interface AssetOverviewProps {
asset: TokenI;
displayBuyButton?: boolean;
displaySwapsButton?: boolean;
+ swapsIsLive?: boolean;
}
const AssetOverview: React.FC = ({
asset,
displayBuyButton,
displaySwapsButton,
+ swapsIsLive,
}: AssetOverviewProps) => {
const navigation = useNavigation();
const [timePeriod, setTimePeriod] = React.useState('1d');
@@ -434,6 +436,7 @@ const AssetOverview: React.FC = ({
{
+ describe('Header', () => {
+ it('renders header correctly when asset name and symbol are provided', () => {
+ const props = {
+ ...mockProps,
+ asset: {
+ ...mockProps.asset,
+ ticker: '',
+ },
+ };
+
+ const { getByText } = render();
+
+ expect(
+ getByText(`${mockProps.asset.name} (${mockProps.asset.symbol})`),
+ ).toBeTruthy();
+ });
+
+ it('renders header correctly when name not provided and symbol is provided', () => {
+ const props = {
+ ...mockProps,
+ asset: {
+ ...mockProps.asset,
+ name: '',
+ ticker: '',
+ },
+ };
+
+ const { getByText } = render();
+
+ expect(getByText(`${mockProps.asset.symbol}`)).toBeTruthy();
+ });
+
+ it('renders header correctly when name and ticker are provided', () => {
+ const { getByText } = render();
+
+ expect(
+ getByText(`${mockProps.asset.name} (${mockProps.asset.ticker})`),
+ ).toBeTruthy();
+ });
+ });
+
+ it('shows loading state when isLoading is true', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('loading-price-diff')).toBeTruthy();
+ });
+});
diff --git a/app/components/UI/AssetOverview/Price/Price.tsx b/app/components/UI/AssetOverview/Price/Price.tsx
index 9e65e259d4c..1fdc4db8be4 100644
--- a/app/components/UI/AssetOverview/Price/Price.tsx
+++ b/app/components/UI/AssetOverview/Price/Price.tsx
@@ -75,7 +75,7 @@ const Price = ({
: priceDiff;
const { styles } = useStyles(styleSheet, { priceDiff: diff });
-
+ const ticker = asset.ticker || asset.symbol;
return (
<>
@@ -84,10 +84,10 @@ const Price = ({
variant={TextVariant.BodyMDMedium}
color={TextColor.Alternative}
>
- {asset.name} ({asset.symbol})
+ {asset.name} ({ticker})
) : (
- {asset.symbol}
+ {ticker}
)}
{!isNaN(price) && (
{isLoading ? (
-
+
diff --git a/app/components/UI/BrowserBottomBar/__snapshots__/index.test.tsx.snap b/app/components/UI/BrowserBottomBar/__snapshots__/index.test.tsx.snap
index a1ea3530ab2..be03def149b 100644
--- a/app/components/UI/BrowserBottomBar/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/BrowserBottomBar/__snapshots__/index.test.tsx.snap
@@ -13,7 +13,7 @@ exports[`BrowserBottomBar should render correctly 1`] = `
},
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderTopWidth": 0.5,
"flex": 0,
"flexDirection": "row",
diff --git a/app/components/UI/BrowserUrlBar/__snapshots__/BrowserUrlBar.test.tsx.snap b/app/components/UI/BrowserUrlBar/__snapshots__/BrowserUrlBar.test.tsx.snap
index d4cd3407d2a..957386ff187 100644
--- a/app/components/UI/BrowserUrlBar/__snapshots__/BrowserUrlBar.test.tsx.snap
+++ b/app/components/UI/BrowserUrlBar/__snapshots__/BrowserUrlBar.test.tsx.snap
@@ -39,7 +39,7 @@ exports[`BrowserUrlBar should render correctly 1`] = `
onChangeText={[Function]}
onFocus={[Function]}
onSubmitEditing={[Function]}
- placeholder="Search or Type URL"
+ placeholder="Search by site or address"
placeholderTextColor="#9fa6ae"
returnKeyType="go"
selectTextOnFocus={true}
@@ -200,7 +200,7 @@ exports[`BrowserUrlBar should render correctly when url bar is not focused 1`] =
onChangeText={[Function]}
onFocus={[Function]}
onSubmitEditing={[Function]}
- placeholder="Search or Type URL"
+ placeholder="Search by site or address"
placeholderTextColor="#9fa6ae"
returnKeyType="go"
selectTextOnFocus={true}
@@ -322,7 +322,7 @@ exports[`BrowserUrlBar should render correctly when url bar is not focused 1`] =
"height": 32,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
diff --git a/app/components/UI/CollectibleContractElement/index.js b/app/components/UI/CollectibleContractElement/index.js
deleted file mode 100644
index 25eddc35f18..00000000000
--- a/app/components/UI/CollectibleContractElement/index.js
+++ /dev/null
@@ -1,334 +0,0 @@
-import React, { useEffect, useState, useCallback, useRef } from 'react';
-import PropTypes from 'prop-types';
-import { StyleSheet, View, TouchableOpacity, Alert } from 'react-native';
-import { connect } from 'react-redux';
-import { fontStyles } from '../../../styles/common';
-import CollectibleMedia from '../CollectibleMedia';
-import Device from '../../../util/device';
-import Text from '../../Base/Text';
-import ActionSheet from '@metamask/react-native-actionsheet';
-import { strings } from '../../../../locales/i18n';
-import Engine from '../../../core/Engine';
-import { removeFavoriteCollectible } from '../../../actions/collectibles';
-import { collectibleContractsSelector } from '../../../reducers/collectibles';
-import { useTheme } from '../../../util/theme';
-import { selectChainId } from '../../../selectors/networkController';
-import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController';
-import Icon, {
- IconName,
- IconColor,
- IconSize,
-} from '../../../component-library/components/Icons/Icon';
-import {
- MetaMetricsEvents,
- useMetrics,
-} from '../../../components/hooks/useMetrics';
-import { getDecimalChainId } from '../../../util/networks';
-
-const DEVICE_WIDTH = Device.getDeviceWidth();
-const COLLECTIBLE_WIDTH = (DEVICE_WIDTH - 30 - 16) / 3;
-
-const createStyles = (colors, brandColors) =>
- StyleSheet.create({
- itemWrapper: {
- paddingHorizontal: 15,
- paddingBottom: 16,
- },
- collectibleContractIcon: { width: 30, height: 30 },
- collectibleContractIconContainer: { marginHorizontal: 8, borderRadius: 30 },
- titleContainer: {
- flex: 1,
- flexDirection: 'row',
- },
- verticalAlignedContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- titleText: {
- fontSize: 18,
- color: colors.text.default,
- ...fontStyles.normal,
- },
- collectibleIcon: {
- width: COLLECTIBLE_WIDTH,
- height: COLLECTIBLE_WIDTH,
- },
- collectibleInTheMiddle: {
- marginHorizontal: 8,
- },
- collectiblesRowContainer: {
- flex: 1,
- flexDirection: 'row',
- marginTop: 15,
- },
- collectibleBox: {
- flex: 1,
- flexDirection: 'row',
- },
- favoritesLogoWrapper: {
- flex: 1,
- flexDirection: 'row',
- justifyContent: 'center',
- alignItems: 'center',
- width: 32,
- height: 32,
- borderRadius: 16,
- backgroundColor: brandColors.yellow500,
- },
- });
-
-const splitIntoSubArrays = (array, count) => {
- const newArray = [];
- while (array.length > 0) {
- newArray.push(array.splice(0, count));
- }
- return newArray;
-};
-
-/**
- * Customizable view to render assets in lists
- */
-function CollectibleContractElement({
- asset,
- contractCollectibles,
- collectiblesVisible: propsCollectiblesVisible,
- onPress,
- collectibleContracts,
- chainId,
- selectedAddress,
- removeFavoriteCollectible,
-}) {
- const [collectiblesGrid, setCollectiblesGrid] = useState([]);
- const [collectiblesVisible, setCollectiblesVisible] = useState(
- propsCollectiblesVisible,
- );
- const actionSheetRef = useRef();
- const longPressedCollectible = useRef(null);
- const { colors, themeAppearance, brandColors } = useTheme();
- const styles = createStyles(colors, brandColors);
- const { trackEvent, createEventBuilder } = useMetrics();
-
- const toggleCollectibles = useCallback(() => {
- setCollectiblesVisible(!collectiblesVisible);
- }, [collectiblesVisible, setCollectiblesVisible]);
-
- const onPressCollectible = useCallback(
- (collectible) => {
- const contractName = collectibleContracts.find(
- ({ address }) => address === collectible.address,
- )?.name;
- onPress(collectible, contractName || collectible.name);
- },
- [collectibleContracts, onPress],
- );
-
- const onLongPressCollectible = useCallback((collectible) => {
- actionSheetRef.current.show();
- longPressedCollectible.current = collectible;
- }, []);
-
- const removeNft = () => {
- const { NftController } = Engine.context;
- removeFavoriteCollectible(
- selectedAddress,
- chainId,
- longPressedCollectible.current,
- );
- NftController.removeAndIgnoreNft(
- longPressedCollectible.current.address,
- longPressedCollectible.current.tokenId,
- );
- trackEvent(
- createEventBuilder(MetaMetricsEvents.COLLECTIBLE_REMOVED)
- .addProperties({
- chain_id: getDecimalChainId(chainId),
- })
- .build(),
- );
- Alert.alert(
- strings('wallet.collectible_removed_title'),
- strings('wallet.collectible_removed_desc'),
- );
- };
-
- const refreshMetadata = () => {
- const { NftController } = Engine.context;
-
- NftController.addNft(
- longPressedCollectible.current.address,
- longPressedCollectible.current.tokenId,
- );
- };
-
- const handleMenuAction = (index) => {
- if (index === 1) {
- removeNft();
- } else if (index === 0) {
- refreshMetadata();
- }
- };
-
- const renderCollectible = useCallback(
- (collectible, index) => {
- if (!collectible) return null;
- const name =
- collectible.name ||
- collectibleContracts.find(
- ({ address }) => address === collectible.address,
- )?.name;
- const onPress = () => onPressCollectible({ ...collectible, name });
- const onLongPress = () =>
- !asset.favorites
- ? onLongPressCollectible({ ...collectible, name })
- : null;
- return (
-
-
-
-
-
-
-
- );
- },
- [
- asset.favorites,
- collectibleContracts,
- onPressCollectible,
- onLongPressCollectible,
- styles,
- ],
- );
-
- useEffect(() => {
- const temp = splitIntoSubArrays(contractCollectibles, 3);
- setCollectiblesGrid(temp);
- }, [contractCollectibles, setCollectiblesGrid]);
-
- return (
-
-
-
-
-
-
- {!asset.favorites ? (
-
- ) : (
-
-
-
- )}
-
-
-
- {asset?.name || strings('collectible.untitled_collection')}
-
-
-
- {collectiblesVisible && (
-
- {collectiblesGrid.map((row, i) => (
-
- {row.map((collectible, index) =>
- renderCollectible({ ...collectible, logo: asset.logo }, index),
- )}
-
- ))}
-
- )}
-
-
- );
-}
-
-CollectibleContractElement.propTypes = {
- /**
- * Object being rendered
- */
- asset: PropTypes.object,
- /**
- * Array of collectibles
- */
- contractCollectibles: PropTypes.array,
- /**
- * Whether the collectibles are visible or not
- */
- collectiblesVisible: PropTypes.bool,
- /**
- * Called when the collectible is pressed
- */
- onPress: PropTypes.func,
- collectibleContracts: PropTypes.array,
- /**
- * Selected address
- */
- selectedAddress: PropTypes.string,
- /**
- * Chain id
- */
- chainId: PropTypes.string,
- /**
- * Dispatch remove collectible from favorites action
- */
- removeFavoriteCollectible: PropTypes.func,
-};
-
-const mapStateToProps = (state) => ({
- collectibleContracts: collectibleContractsSelector(state),
- chainId: selectChainId(state),
- selectedAddress: selectSelectedInternalAccountFormattedAddress(state),
-});
-
-const mapDispatchToProps = (dispatch) => ({
- removeFavoriteCollectible: (selectedAddress, chainId, collectible) =>
- dispatch(removeFavoriteCollectible(selectedAddress, chainId, collectible)),
-});
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps,
-)(CollectibleContractElement);
diff --git a/app/components/UI/CollectibleContractInformation/__snapshots__/index.test.tsx.snap b/app/components/UI/CollectibleContractInformation/__snapshots__/index.test.tsx.snap
deleted file mode 100644
index 7036107cf0d..00000000000
--- a/app/components/UI/CollectibleContractInformation/__snapshots__/index.test.tsx.snap
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CollectibleContractInformation should render correctly 1`] = `
-
-
-
-`;
diff --git a/app/components/UI/CollectibleContractInformation/index.js b/app/components/UI/CollectibleContractInformation/index.js
deleted file mode 100644
index dfd9733e0f1..00000000000
--- a/app/components/UI/CollectibleContractInformation/index.js
+++ /dev/null
@@ -1,229 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import {
- ScrollView,
- TouchableOpacity,
- StyleSheet,
- Text,
- View,
- SafeAreaView,
- InteractionManager,
- Image,
-} from 'react-native';
-import { fontStyles } from '../../../styles/common';
-import { strings } from '../../../../locales/i18n';
-import Device from '../../../util/device';
-import { connect } from 'react-redux';
-import { isMainNet } from '../../../util/networks';
-import { ThemeContext, mockTheme } from '../../../util/theme';
-import { selectChainId } from '../../../selectors/networkController';
-
-const createStyles = (colors) =>
- StyleSheet.create({
- wrapper: {
- backgroundColor: colors.background.default,
- borderRadius: 10,
- minHeight: 450,
- },
- titleWrapper: {
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderColor: colors.border.muted,
- },
- title: {
- textAlign: 'center',
- fontSize: 18,
- marginVertical: 12,
- marginHorizontal: 20,
- color: colors.text.default,
- ...fontStyles.bold,
- },
- label: {
- marginTop: 0,
- borderColor: colors.border.muted,
- ...fontStyles.bold,
- color: colors.text.default,
- },
- informationWrapper: {
- flex: 1,
- paddingHorizontal: 20,
- },
- content: {
- fontSize: 16,
- color: colors.text.alternative,
- paddingTop: 10,
- ...fontStyles.normal,
- },
- address: {
- fontSize: 12,
- },
- row: {
- marginVertical: 10,
- },
- footer: {
- borderTopWidth: StyleSheet.hairlineWidth,
- borderColor: colors.border.muted,
- height: 60,
- justifyContent: 'center',
- flexDirection: 'row',
- alignItems: 'center',
- },
- footerButton: {
- flex: 1,
- alignContent: 'center',
- alignItems: 'center',
- justifyContent: 'center',
- height: 60,
- },
- closeButton: {
- fontSize: 16,
- color: colors.primary.default,
- ...fontStyles.normal,
- },
- opensea: {
- fontSize: 8,
- textAlignVertical: 'center',
- paddingRight: 5,
- marginTop: Device.isAndroid() ? -2 : 4,
- color: colors.text.alternative,
- ...fontStyles.light,
- },
- credits: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- textAlign: 'center',
- },
- openSeaLogo: {
- width: 80,
- height: 20,
- resizeMode: 'contain',
- },
- creditsView: {
- alignItems: 'center',
- marginTop: 15,
- },
- creditsElements: {
- flexDirection: 'row',
- },
- });
-
-const openSeaLogo = require('../../../images/opensea-logo-flat-colored-blue.png'); // eslint-disable-line
-
-/**
- * View that contains a collectible contract information as description, total supply and address
- */
-class CollectibleContractInformation extends PureComponent {
- static propTypes = {
- /**
- * Navigation object required to push
- * the Asset detail view
- */
- navigation: PropTypes.object,
- /**
- * An function to handle the close event
- */
- onClose: PropTypes.func,
- /**
- * Collectible contract object
- */
- collectibleContract: PropTypes.object,
- /**
- * The chain ID for the current selected network
- */
- chainId: PropTypes.string.isRequired,
- };
-
- closeModal = () => {
- this.props.onClose(true);
- };
-
- goToOpenSea = () => {
- const openSeaUrl = 'https://opensea.io/';
- InteractionManager.runAfterInteractions(() => {
- this.closeModal();
- this.props.navigation.push('Webview', {
- screen: 'SimpleWebview',
- params: {
- url: openSeaUrl,
- title: 'OpenSea',
- },
- });
- });
- };
-
- render = () => {
- const {
- collectibleContract: { name, description, totalSupply, address },
- chainId,
- } = this.props;
- const colors = this.context.colors || mockTheme.colors;
- const styles = createStyles(colors);
- const is_main_net = isMainNet(chainId);
-
- return (
-
-
-
- {name}
-
-
-
- {description && (
-
-
- {strings('asset_overview.description')}
-
- {description}
-
- )}
- {totalSupply && (
-
-
- {strings('asset_overview.totalSupply')}
-
- {totalSupply}
-
- )}
-
-
- {strings('asset_overview.address')}
-
- {address}
-
- {is_main_net && (
-
-
-
-
- {strings('collectible.powered_by_opensea')}
-
-
-
-
-
- )}
-
-
-
-
- {strings('networks.close')}
-
-
-
- );
- };
-}
-
-const mapStateToProps = (state) => ({
- chainId: selectChainId(state),
-});
-
-CollectibleContractInformation.contextType = ThemeContext;
-
-export default connect(mapStateToProps)(CollectibleContractInformation);
diff --git a/app/components/UI/CollectibleContractInformation/index.test.tsx b/app/components/UI/CollectibleContractInformation/index.test.tsx
deleted file mode 100644
index 2629113abd8..00000000000
--- a/app/components/UI/CollectibleContractInformation/index.test.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import CollectibleContractInformation from './';
-import configureMockStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
-import { backgroundState } from '../../../util/test/initial-root-state';
-
-const mockStore = configureMockStore();
-const initialState = {
- engine: {
- backgroundState,
- },
-};
-const store = mockStore(initialState);
-
-describe('CollectibleContractInformation', () => {
- it('should render correctly', () => {
- const wrapper = shallow(
-
-
- ,
- );
- expect(wrapper).toMatchSnapshot();
- });
-});
diff --git a/app/components/UI/CollectibleContractOverview/__snapshots__/index.test.tsx.snap b/app/components/UI/CollectibleContractOverview/__snapshots__/index.test.tsx.snap
deleted file mode 100644
index 18873220956..00000000000
--- a/app/components/UI/CollectibleContractOverview/__snapshots__/index.test.tsx.snap
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CollectibleContractOverview should render correctly 1`] = `
-
-
-
-`;
diff --git a/app/components/UI/CollectibleContractOverview/index.js b/app/components/UI/CollectibleContractOverview/index.js
deleted file mode 100644
index 94942c48f33..00000000000
--- a/app/components/UI/CollectibleContractOverview/index.js
+++ /dev/null
@@ -1,177 +0,0 @@
-import React, { PureComponent } from 'react';
-import { StyleSheet, Text, View } from 'react-native';
-import PropTypes from 'prop-types';
-import { fontStyles } from '../../../styles/common';
-import { strings } from '../../../../locales/i18n';
-import CollectibleMedia from '../CollectibleMedia';
-import AssetActionButton from '../AssetOverview/AssetActionButton';
-import Device from '../../../util/device';
-import { toggleCollectibleContractModal } from '../../../actions/modals';
-import { connect } from 'react-redux';
-import collectiblesTransferInformation from '../../../util/collectibles-transfer';
-import { newAssetTransaction } from '../../../actions/transaction';
-import { toLowerCaseEquals } from '../../../util/general';
-import { collectiblesSelector } from '../../../reducers/collectibles';
-import { ThemeContext, mockTheme } from '../../../util/theme';
-import { TokenOverviewSelectorsIDs } from '../../../../e2e/selectors/wallet/TokenOverview.selectors';
-import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
-
-const createStyles = (colors) =>
- StyleSheet.create({
- wrapper: {
- flex: 1,
- paddingHorizontal: 20,
- borderBottomWidth: StyleSheet.hairlineWidth,
- borderBottomColor: colors.border.muted,
- alignContent: 'center',
- alignItems: 'center',
- paddingBottom: 30,
- },
- assetLogo: {
- marginTop: 20,
- },
- information: {
- flex: 1,
- flexDirection: 'row',
- marginTop: 10,
- marginBottom: 20,
- },
- name: {
- fontSize: 30,
- textAlign: 'center',
- color: colors.text.default,
- ...fontStyles.normal,
- },
- actions: {
- width: Device.isSmallDevice() ? '65%' : '50%',
- justifyContent: 'space-around',
- alignItems: 'flex-start',
- flexDirection: 'row',
- },
- });
-
-/**
- * View that displays a specific collectible contract
- * including the overview (name, address, symbol, logo, description, total supply)
- */
-class CollectibleContractOverview extends PureComponent {
- static propTypes = {
- /**
- * Object that represents the asset to be displayed
- */
- collectibleContract: PropTypes.object,
- /**
- * Array of ERC721 assets
- */
- collectibles: PropTypes.array,
- /**
- * Navigation object required to push
- * the Asset detail view
- */
- navigation: PropTypes.object,
- /**
- * How many collectibles are owned by the user
- */
- ownerOf: PropTypes.number,
- /**
- * Action that sets a collectible contract type transaction
- */
- toggleCollectibleContractModal: PropTypes.func.isRequired,
- /**
- * Start transaction with asset
- */
- newAssetTransaction: PropTypes.func,
- };
-
- onAdd = () => {
- const { navigation, collectibleContract } = this.props;
- navigation.push('AddAsset', {
- assetType: 'collectible',
- collectibleContract,
- });
- };
-
- onSend = () => {
- const { collectibleContract, collectibles } = this.props;
- const collectible = collectibles.find((collectible) =>
- toLowerCaseEquals(collectible.address, collectibleContract.address),
- );
- this.props.newAssetTransaction(collectible);
- this.props.navigation.navigate('SendFlowView');
- };
-
- onInfo = () => this.props.toggleCollectibleContractModal();
-
- renderLogo = () => {
- const {
- collectibleContract: { logo, address },
- } = this.props;
- return ;
- };
-
- render() {
- const {
- collectibleContract: { name, address },
- ownerOf,
- } = this.props;
- const colors = this.context.colors || mockTheme.colors;
- const styles = createStyles(colors);
- const lowerAddress = address.toLowerCase();
- const leftActionButtonText =
- lowerAddress in collectiblesTransferInformation
- ? collectiblesTransferInformation[lowerAddress].tradable &&
- strings('asset_overview.send_button')
- : strings('asset_overview.send_button');
- return (
-
- {this.renderLogo()}
-
-
- {ownerOf} {name}
-
-
-
-
-
-
-
-
-
- );
- }
-}
-
-const mapStateToProps = (state) => ({
- collectibles: collectiblesSelector(state),
-});
-
-const mapDispatchToProps = (dispatch) => ({
- toggleCollectibleContractModal: () =>
- dispatch(toggleCollectibleContractModal()),
- newAssetTransaction: (selectedAsset) =>
- dispatch(newAssetTransaction(selectedAsset)),
-});
-
-CollectibleContractOverview.contextType = ThemeContext;
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps,
-)(CollectibleContractOverview);
diff --git a/app/components/UI/CollectibleContractOverview/index.test.tsx b/app/components/UI/CollectibleContractOverview/index.test.tsx
deleted file mode 100644
index 94b7f7cf9c9..00000000000
--- a/app/components/UI/CollectibleContractOverview/index.test.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react';
-import CollectibleContractOverview from './';
-import configureMockStore from 'redux-mock-store';
-import { shallow } from 'enzyme';
-import { Provider } from 'react-redux';
-import { CHAIN_IDS } from '@metamask/transaction-controller';
-import { backgroundState } from '../../../util/test/initial-root-state';
-import { mockNetworkState } from '../../../util/test/network';
-
-const mockStore = configureMockStore();
-
-const initialState = {
- engine: {
- backgroundState: {
- ...backgroundState,
- NetworkController: {
- ...mockNetworkState({
- chainId: CHAIN_IDS.MAINNET,
- id: 'mainnet',
- nickname: 'Ethereum Mainnet',
- ticker: 'ETH',
- }),
- },
- },
- },
-};
-const store = mockStore(initialState);
-
-describe('CollectibleContractOverview', () => {
- it('should render correctly', () => {
- const wrapper = shallow(
-
-
- ,
- );
- expect(wrapper).toMatchSnapshot();
- });
-});
diff --git a/app/components/UI/CollectibleContracts/__snapshots__/index.test.tsx.snap b/app/components/UI/CollectibleContracts/__snapshots__/index.test.tsx.snap
deleted file mode 100644
index ab35bdf6fa8..00000000000
--- a/app/components/UI/CollectibleContracts/__snapshots__/index.test.tsx.snap
+++ /dev/null
@@ -1,32 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CollectibleContracts should render correctly 1`] = `
-
-
-
-`;
diff --git a/app/components/UI/CollectibleContracts/constants.ts b/app/components/UI/CollectibleContracts/constants.ts
deleted file mode 100644
index 2682130644b..00000000000
--- a/app/components/UI/CollectibleContracts/constants.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const RefreshTestId = 'refreshControl';
-export const SpinnerTestId = 'spinner';
diff --git a/app/components/UI/CollectibleContracts/index.js b/app/components/UI/CollectibleContracts/index.js
deleted file mode 100644
index 3bc6e7776c2..00000000000
--- a/app/components/UI/CollectibleContracts/index.js
+++ /dev/null
@@ -1,462 +0,0 @@
-import React, { useState, useEffect, useCallback } from 'react';
-import PropTypes from 'prop-types';
-import {
- TouchableOpacity,
- StyleSheet,
- View,
- Image,
- FlatList,
- RefreshControl,
- ActivityIndicator,
-} from 'react-native';
-import { connect } from 'react-redux';
-import { fontStyles } from '../../../styles/common';
-import { strings } from '../../../../locales/i18n';
-import Engine from '../../../core/Engine';
-import CollectibleContractElement from '../CollectibleContractElement';
-import { MetaMetricsEvents } from '../../../core/Analytics';
-import {
- collectibleContractsSelector,
- collectiblesSelector,
- favoritesCollectiblesSelector,
- isNftFetchingProgressSelector,
-} from '../../../reducers/collectibles';
-import { removeFavoriteCollectible } from '../../../actions/collectibles';
-import Text from '../../Base/Text';
-import AppConstants from '../../../core/AppConstants';
-import { toLowerCaseEquals } from '../../../util/general';
-import { compareTokenIds } from '../../../util/tokens';
-import CollectibleDetectionModal from '../CollectibleDetectionModal';
-import { useTheme } from '../../../util/theme';
-import { MAINNET } from '../../../constants/network';
-import {
- selectChainId,
- selectProviderType,
-} from '../../../selectors/networkController';
-import {
- selectDisplayNftMedia,
- selectIsIpfsGatewayEnabled,
- selectUseNftDetection,
-} from '../../../selectors/preferencesController';
-import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController';
-import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
-import { useMetrics } from '../../../components/hooks/useMetrics';
-import { RefreshTestId, SpinnerTestId } from './constants';
-import { debounce } from 'lodash';
-
-const createStyles = (colors) =>
- StyleSheet.create({
- wrapper: {
- backgroundColor: colors.background.default,
- flex: 1,
- marginTop: 16,
- },
- emptyView: {
- justifyContent: 'center',
- alignItems: 'center',
- },
- addText: {
- fontSize: 14,
- color: colors.primary.default,
- ...fontStyles.normal,
- },
- footer: {
- flex: 1,
- alignItems: 'center',
- marginTop: 8,
- },
- emptyContainer: {
- flex: 1,
- alignItems: 'center',
- },
- emptyImageContainer: {
- width: 76,
- height: 76,
- marginTop: 30,
- marginBottom: 12,
- tintColor: colors.icon.muted,
- },
- emptyTitleText: {
- fontSize: 24,
- color: colors.text.alternative,
- },
- emptyText: {
- color: colors.text.alternative,
- marginBottom: 8,
- fontSize: 14,
- },
- spinner: {
- marginBottom: 8,
- },
- });
-
-const debouncedNavigation = debounce((navigation, collectible) => {
- navigation.navigate('NftDetails', { collectible });
-}, 200);
-
-/**
- * View that renders a list of CollectibleContract
- * ERC-721 and ERC-1155
- */
-const CollectibleContracts = ({
- selectedAddress,
- chainId,
- networkType,
- navigation,
- collectibleContracts,
- collectibles: allCollectibles,
- isNftFetchingProgress,
- favoriteCollectibles,
- removeFavoriteCollectible,
- useNftDetection,
- isIpfsGatewayEnabled,
- displayNftMedia,
-}) => {
- const collectibles = allCollectibles.filter(
- (singleCollectible) => singleCollectible.isCurrentlyOwned === true,
- );
- const { colors } = useTheme();
- const { trackEvent, createEventBuilder } = useMetrics();
- const styles = createStyles(colors);
- const [isAddNFTEnabled, setIsAddNFTEnabled] = useState(true);
- const [refreshing, setRefreshing] = useState(false);
-
- const isCollectionDetectionBannerVisible =
- networkType === MAINNET && !useNftDetection;
-
- const onItemPress = useCallback(
- (collectible) => {
- debouncedNavigation(navigation, collectible);
- },
- [navigation],
- );
-
- /**
- * Method that checks if the collectible is inside the collectibles array. If it is not it means the
- * collectible has been ignored, hence we should not call the updateMetadata which executes the addNft fct
- *
- * @returns Boolean indicating if the collectible is ignored or not.
- */
- const isCollectibleIgnored = useCallback(
- (collectible) => {
- const found = collectibles.find(
- (elm) =>
- elm.address === collectible.address &&
- elm.tokenId === collectible.tokenId,
- );
- if (found) return false;
- return true;
- },
- [collectibles],
- );
-
- /**
- * Method to check the token id data type of the current collectibles.
- *
- * @param collectible - Collectible object.
- * @returns Boolean indicating if the collectible should be updated.
- */
- const shouldUpdateCollectibleMetadata = (collectible) =>
- typeof collectible.tokenId === 'number' ||
- (typeof collectible.tokenId === 'string' && !isNaN(collectible.tokenId));
-
- const updateAllCollectibleMetadata = useCallback(
- async (collectibles) => {
- const { NftController } = Engine.context;
- // Filter out ignored collectibles
- const filteredcollectibles = collectibles.filter(
- (collectible) => !isCollectibleIgnored(collectible),
- );
-
- // filter removable collectible
- const removable = filteredcollectibles.filter((single) =>
- String(single.tokenId).includes('e+'),
- );
- const updatable = filteredcollectibles.filter(
- (single) => !String(single.tokenId).includes('e+'),
- );
-
- removable.forEach((elm) => {
- removeFavoriteCollectible(selectedAddress, chainId, elm);
- });
-
- filteredcollectibles.forEach((collectible) => {
- if (String(collectible.tokenId).includes('e+')) {
- removeFavoriteCollectible(selectedAddress, chainId, collectible);
- }
- });
-
- if (updatable.length !== 0) {
- await NftController.updateNftMetadata({
- nfts: updatable,
- userAddress: selectedAddress,
- });
- }
- },
- [isCollectibleIgnored, removeFavoriteCollectible, chainId, selectedAddress],
- );
-
- useEffect(() => {
- if (!isIpfsGatewayEnabled && !displayNftMedia) {
- return;
- }
- // TO DO: Move this fix to the controllers layer
- const updatableCollectibles = collectibles.filter((single) =>
- shouldUpdateCollectibleMetadata(single),
- );
- if (updatableCollectibles.length !== 0 && !useNftDetection) {
- updateAllCollectibleMetadata(updatableCollectibles);
- }
- }, [
- collectibles,
- updateAllCollectibleMetadata,
- isIpfsGatewayEnabled,
- displayNftMedia,
- useNftDetection,
- ]);
-
- const goToAddCollectible = useCallback(() => {
- setIsAddNFTEnabled(false);
- navigation.push('AddAsset', { assetType: 'collectible' });
- trackEvent(
- createEventBuilder(MetaMetricsEvents.WALLET_ADD_COLLECTIBLES).build(),
- );
- setIsAddNFTEnabled(true);
- }, [navigation, trackEvent, createEventBuilder]);
-
- const renderFooter = useCallback(
- () => (
-
- {isNftFetchingProgress ? (
-
- ) : null}
-
-
- {strings('wallet.no_collectibles')}
-
-
-
- {strings('wallet.add_collectibles')}
-
-
-
- ),
- [goToAddCollectible, isAddNFTEnabled, styles, isNftFetchingProgress],
- );
-
- const renderCollectibleContract = useCallback(
- (item, index) => {
- const contractCollectibles = collectibles?.filter((collectible) =>
- toLowerCaseEquals(collectible.address, item.address),
- );
- return (
-
- );
- },
- [collectibles, onItemPress],
- );
-
- const renderFavoriteCollectibles = useCallback(() => {
- const filteredCollectibles = favoriteCollectibles.map((collectible) =>
- collectibles.find(
- ({ tokenId, address }) =>
- compareTokenIds(collectible.tokenId, tokenId) &&
- collectible.address === address,
- ),
- );
- return (
- Boolean(filteredCollectibles.length) && (
-
- )
- );
- }, [favoriteCollectibles, collectibles, onItemPress]);
- const onRefresh = useCallback(async () => {
- requestAnimationFrame(async () => {
- setRefreshing(true);
- const { NftDetectionController, NftController } = Engine.context;
- const actions = [
- NftDetectionController.detectNfts(),
- NftController.checkAndUpdateAllNftsOwnershipStatus(),
- ];
- await Promise.allSettled(actions);
- setRefreshing(false);
- });
- }, [setRefreshing]);
-
- const goToLearnMore = useCallback(
- () =>
- navigation.navigate('Webview', {
- screen: 'SimpleWebview',
- params: { url: AppConstants.URLS.NFT },
- }),
- [navigation],
- );
-
- const renderEmpty = useCallback(
- () => (
-
-
-
- {strings('wallet.no_nfts_yet')}
-
-
- {strings('wallet.learn_more')}
-
-
- ),
- [goToLearnMore, styles],
- );
-
- const renderList = useCallback(
- () => (
-
- {isCollectionDetectionBannerVisible && (
-
-
-
- )}
- {renderFavoriteCollectibles()}
- >
- }
- data={collectibleContracts}
- renderItem={({ item, index }) => renderCollectibleContract(item, index)}
- keyExtractor={(_, index) => index.toString()}
- testID={RefreshTestId}
- refreshControl={
-
- }
- ListEmptyComponent={renderEmpty()}
- ListFooterComponent={renderFooter()}
- />
- ),
- [
- renderFavoriteCollectibles,
- collectibleContracts,
- colors.primary.default,
- colors.icon.default,
- refreshing,
- onRefresh,
- renderCollectibleContract,
- renderFooter,
- renderEmpty,
- isCollectionDetectionBannerVisible,
- styles.emptyView,
- ],
- );
-
- return (
-
- {renderList()}
-
- );
-};
-
-CollectibleContracts.propTypes = {
- /**
- * Network type
- */
- networkType: PropTypes.string,
- /**
- * Chain id
- */
- chainId: PropTypes.string,
- /**
- * Selected address
- */
- selectedAddress: PropTypes.string,
- /**
- * Array of collectibleContract objects
- */
- collectibleContracts: PropTypes.array,
- /**
- * Array of collectibles objects
- */
- collectibles: PropTypes.array,
- /**
- * boolean indicating if fetching status is
- * still in progress
- */
- isNftFetchingProgress: PropTypes.bool,
- /**
- * Navigation object required to push
- * the Asset detail view
- */
- navigation: PropTypes.object,
- /**
- * Object of collectibles
- */
- favoriteCollectibles: PropTypes.array,
- /**
- * Dispatch remove collectible from favorites action
- */
- removeFavoriteCollectible: PropTypes.func,
- /**
- * Boolean to show if NFT detection is enabled
- */
- useNftDetection: PropTypes.bool,
- /**
- * Boolean to show content stored on IPFS
- */
- isIpfsGatewayEnabled: PropTypes.bool,
- /**
- * Boolean to show Nfts media stored on third parties
- */
- displayNftMedia: PropTypes.bool,
-};
-
-const mapStateToProps = (state) => ({
- networkType: selectProviderType(state),
- chainId: selectChainId(state),
- selectedAddress: selectSelectedInternalAccountFormattedAddress(state),
- useNftDetection: selectUseNftDetection(state),
- collectibleContracts: collectibleContractsSelector(state),
- collectibles: collectiblesSelector(state),
- isNftFetchingProgress: isNftFetchingProgressSelector(state),
- favoriteCollectibles: favoritesCollectiblesSelector(state),
- isIpfsGatewayEnabled: selectIsIpfsGatewayEnabled(state),
- displayNftMedia: selectDisplayNftMedia(state),
-});
-
-const mapDispatchToProps = (dispatch) => ({
- removeFavoriteCollectible: (selectedAddress, chainId, collectible) =>
- dispatch(removeFavoriteCollectible(selectedAddress, chainId, collectible)),
-});
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps,
-)(CollectibleContracts);
diff --git a/app/components/UI/CollectibleContracts/index.test.tsx b/app/components/UI/CollectibleContracts/index.test.tsx
deleted file mode 100644
index 8ed9f29a830..00000000000
--- a/app/components/UI/CollectibleContracts/index.test.tsx
+++ /dev/null
@@ -1,601 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import CollectibleContracts from './';
-import configureMockStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
-import { backgroundState } from '../../../util/test/initial-root-state';
-import renderWithProvider, {
- DeepPartial,
-} from '../../../util/test/renderWithProvider';
-import { act } from '@testing-library/react-hooks';
-
-// eslint-disable-next-line import/no-namespace
-import * as allSelectors from '../../../../app/reducers/collectibles/index.js';
-import { cleanup, waitFor } from '@testing-library/react-native';
-import Engine from '../../../core/Engine';
-
-import TestHelpers from '../../../../e2e/helpers';
-import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils';
-import { RootState } from '../../../reducers';
-import { mockNetworkState } from '../../../util/test/network';
-import { CHAIN_IDS } from '@metamask/transaction-controller';
-
-jest.mock('@react-navigation/native', () => {
- const actualReactNavigation = jest.requireActual('@react-navigation/native');
- return {
- ...actualReactNavigation,
- useNavigation: () => ({
- navigate: jest.fn(),
- setOptions: jest.fn(),
- goBack: jest.fn(),
- reset: jest.fn(),
- dangerouslyGetParent: () => ({
- pop: jest.fn(),
- }),
- }),
- };
-});
-
-jest.mock('../../../core/Engine', () => ({
- context: {
- NftController: {
- addNft: jest.fn(),
- updateNftMetadata: jest.fn(),
- checkAndUpdateAllNftsOwnershipStatus: jest.fn(),
- },
- NftDetectionController: {
- detectNfts: jest.fn(),
- },
- },
-}));
-
-const mockStore = configureMockStore();
-
-const initialState = {
- collectibles: {
- favorites: {},
- },
- engine: {
- backgroundState,
- },
-};
-const store = mockStore(initialState);
-
-const MOCK_ADDRESS = '0xd018538C87232FF95acbCe4870629b75640a78E7';
-const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([
- MOCK_ADDRESS,
-]);
-
-describe('CollectibleContracts', () => {
- afterEach(cleanup);
- it('should render correctly', () => {
- const wrapper = shallow(
-
-
- ,
- );
- expect(wrapper).toMatchSnapshot();
- });
-
- it('should only get owned collectibles', () => {
- const mockState: DeepPartial = {
- collectibles: {
- favorites: {},
- },
- engine: {
- backgroundState: {
- ...backgroundState,
- NetworkController: {
- ...mockNetworkState({
- chainId: CHAIN_IDS.MAINNET,
- id: 'mainnet',
- nickname: 'Ethereum Mainnet',
- ticker: 'ETH',
- }),
- },
- AccountTrackerController: {
- accounts: { [MOCK_ADDRESS]: { balance: '0' } },
- },
- AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
- NftController: {
- allNfts: {
- [MOCK_ADDRESS.toLowerCase()]: {
- '0x1': [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- description:
- 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
- error: 'Opensea import error',
- favorite: false,
- image: 'https://api.pudgypenguins.io/lil/image/113',
- isCurrentlyOwned: true,
- name: 'Lil Pudgy #113',
- standard: 'ERC721',
- tokenId: '113',
- tokenURI: 'https://api.pudgypenguins.io/lil/113',
- },
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- description:
- 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
- error: 'Opensea import error',
- favorite: false,
- image: 'https://api.pudgypenguins.io/lil/image/113',
- isCurrentlyOwned: false,
- name: 'Lil Pudgy #114',
- standard: 'ERC721',
- tokenId: '114',
- tokenURI: 'https://api.pudgypenguins.io/lil/114',
- },
- ],
- },
- },
- allNftContracts: {
- [MOCK_ADDRESS.toLowerCase()]: {
- '0x1': [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- name: 'MyToken',
- symbol: 'MTK',
- },
- ],
- },
- },
- },
- },
- },
- };
- const { queryByTestId } = renderWithProvider(, {
- state: mockState,
- });
-
- const ownedNft = queryByTestId('collectible-Lil Pudgy #113-113');
- const nonOwnedNft = queryByTestId('collectible-Lil Pudgy #114-114');
-
- expect(ownedNft).toBeTruthy();
- expect(nonOwnedNft).toBeNull();
- });
-
- it('UI refresh changes NFT image when metadata image changes - detection disabled', async () => {
- const collectibleData = [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- name: 'MyToken',
- symbol: 'MTK',
- },
- ];
- const nftItemData = [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- description:
- 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
- error: 'Opensea import error',
- favorite: false,
- image: 'https://api.pudgypenguins.io/lil/image/11222',
- isCurrentlyOwned: true,
- name: 'Lil Pudgy #113',
- standard: 'ERC721',
- tokenId: '113',
- tokenURI: 'https://api.pudgypenguins.io/lil/113',
- },
- ];
-
- const nftItemDataUpdated = [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- description:
- 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
- error: 'Opensea import error',
- favorite: false,
- image: 'https://api.pudgypenguins.io/lil/image/updated.png',
- isCurrentlyOwned: true,
- name: 'Lil Pudgy #113',
- standard: 'ERC721',
- tokenId: '113',
- tokenURI: 'https://api.pudgypenguins.io/lil/113',
- },
- ];
- const mockState: DeepPartial = {
- collectibles: {
- favorites: {},
- },
- engine: {
- backgroundState: {
- ...backgroundState,
- NetworkController: {
- ...mockNetworkState({
- chainId: CHAIN_IDS.MAINNET,
- id: 'mainnet',
- nickname: 'Ethereum Mainnet',
- ticker: 'ETH',
- }),
- },
- AccountTrackerController: {
- accounts: { [MOCK_ADDRESS]: { balance: '0' } },
- },
- PreferencesController: {
- displayNftMedia: true,
- },
- AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
- NftController: {
- allNfts: {
- [MOCK_ADDRESS]: {
- '0x1': [],
- },
- },
- allNftContracts: {
- [MOCK_ADDRESS]: {
- '0x1': [],
- },
- },
- },
- },
- },
- };
-
- const spyOnCollectibles = jest
- .spyOn(allSelectors, 'collectiblesSelector')
- .mockReturnValueOnce(nftItemData)
- .mockReturnValueOnce(nftItemDataUpdated);
- const spyOnContracts = jest
- .spyOn(allSelectors, 'collectibleContractsSelector')
- .mockReturnValue(collectibleData);
- const spyOnUpdateNftMetadata = jest
- .spyOn(Engine.context.NftController, 'updateNftMetadata')
- .mockImplementation(async () => undefined);
-
- const { getByTestId } = renderWithProvider(, {
- state: mockState,
- });
- const nftImageBefore = getByTestId('nft-image');
- expect(nftImageBefore.props.source.uri).toEqual(nftItemData[0].image);
-
- const { queryByTestId } = renderWithProvider(, {
- state: mockState,
- });
-
- await waitFor(() => {
- expect(spyOnUpdateNftMetadata).toHaveBeenCalled();
- const nftImageAfter = queryByTestId('nft-image');
- expect(nftImageAfter.props.source.uri).toEqual(
- nftItemDataUpdated[0].image,
- );
- });
-
- spyOnCollectibles.mockRestore();
- spyOnContracts.mockRestore();
- spyOnUpdateNftMetadata.mockRestore();
- });
-
- it('UI refresh changes NFT image when metadata image changes - detection enabled', async () => {
- const collectibleData = [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- name: 'MyToken',
- symbol: 'MTK',
- },
- ];
- const nftItemData = [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- description:
- 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
- error: 'Opensea import error',
- favorite: false,
- image: 'https://api.pudgypenguins.io/lil/image/11222',
- isCurrentlyOwned: true,
- name: 'Lil Pudgy #113',
- standard: 'ERC721',
- tokenId: '113',
- tokenURI: 'https://api.pudgypenguins.io/lil/113',
- },
- ];
-
- const nftItemDataUpdated = [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- description:
- 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
- error: 'Opensea import error',
- favorite: false,
- image: 'https://api.pudgypenguins.io/lil/image/updated.png',
- isCurrentlyOwned: true,
- name: 'Lil Pudgy #113',
- standard: 'ERC721',
- tokenId: '113',
- tokenURI: 'https://api.pudgypenguins.io/lil/113',
- },
- ];
- const mockState: DeepPartial = {
- collectibles: {
- favorites: {},
- },
- engine: {
- backgroundState: {
- ...backgroundState,
- NetworkController: {
- ...mockNetworkState({
- chainId: CHAIN_IDS.MAINNET,
- id: 'mainnet',
- nickname: 'Ethereum Mainnet',
- ticker: 'ETH',
- }),
- },
- AccountTrackerController: {
- accounts: { [MOCK_ADDRESS]: { balance: '0' } },
- },
- PreferencesController: {
- useNftDetection: true,
- displayNftMedia: true,
- },
- AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
- NftController: {
- allNfts: {
- [MOCK_ADDRESS]: {
- '0x1': [],
- },
- },
- allNftContracts: {
- [MOCK_ADDRESS]: {
- '0x1': [],
- },
- },
- },
- },
- },
- };
-
- const spyOnCollectibles = jest
- .spyOn(allSelectors, 'collectiblesSelector')
- .mockReturnValueOnce(nftItemData)
- .mockReturnValueOnce(nftItemDataUpdated);
- const spyOnContracts = jest
- .spyOn(allSelectors, 'collectibleContractsSelector')
- .mockReturnValue(collectibleData);
- const spyOnUpdateNftMetadata = jest
- .spyOn(Engine.context.NftController, 'updateNftMetadata')
- .mockImplementation(async () => undefined);
-
- const { getByTestId } = renderWithProvider(, {
- state: mockState,
- });
- const nftImageBefore = getByTestId('nft-image');
- expect(nftImageBefore.props.source.uri).toEqual(nftItemData[0].image);
-
- const { queryByTestId } = renderWithProvider(, {
- state: mockState,
- });
-
- await waitFor(() => {
- expect(spyOnUpdateNftMetadata).toHaveBeenCalledTimes(0);
- const nftImageAfter = queryByTestId('nft-image');
- expect(nftImageAfter.props.source.uri).toEqual(
- nftItemDataUpdated[0].image,
- );
- });
-
- spyOnCollectibles.mockRestore();
- spyOnContracts.mockRestore();
- spyOnUpdateNftMetadata.mockRestore();
- });
-
- it('UI pull down experience should call detectNfts when detection is enabled', async () => {
- const collectibleData = [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- name: 'MyToken',
- symbol: 'MTK',
- },
- ];
- const nftItemData = [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- description:
- 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
- error: 'Opensea import error',
- favorite: false,
- image: 'https://api.pudgypenguins.io/lil/image/11222',
- isCurrentlyOwned: true,
- name: 'Lil Pudgy #113',
- standard: 'ERC721',
- tokenId: '113',
- tokenURI: 'https://api.pudgypenguins.io/lil/113',
- },
- ];
-
- const nftItemDataUpdated = [
- {
- address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
- description:
- 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
- error: 'Opensea import error',
- favorite: false,
- image: 'https://api.pudgypenguins.io/lil/image/updated.png',
- isCurrentlyOwned: true,
- name: 'Lil Pudgy #113',
- standard: 'ERC721',
- tokenId: '113',
- tokenURI: 'https://api.pudgypenguins.io/lil/113',
- },
- ];
- const mockState: DeepPartial = {
- collectibles: {
- favorites: {},
- },
- engine: {
- backgroundState: {
- ...backgroundState,
- NetworkController: {
- ...mockNetworkState({
- chainId: CHAIN_IDS.MAINNET,
- id: 'mainnet',
- nickname: 'Ethereum Mainnet',
- ticker: 'ETH',
- }),
- },
- AccountTrackerController: {
- accounts: { [MOCK_ADDRESS]: { balance: '0' } },
- },
- PreferencesController: {
- useNftDetection: true,
- displayNftMedia: true,
- },
- NftController: {
- allNfts: {
- [MOCK_ADDRESS]: {
- '0x1': [],
- },
- },
- allNftContracts: {
- [MOCK_ADDRESS]: {
- '0x1': [],
- },
- },
- },
- },
- },
- };
-
- jest
- .spyOn(allSelectors, 'collectiblesSelector')
- .mockReturnValueOnce(nftItemData)
- .mockReturnValueOnce(nftItemDataUpdated);
- jest
- .spyOn(allSelectors, 'collectibleContractsSelector')
- .mockReturnValue(collectibleData);
- const spyOnUpdateNftMetadata = jest
- .spyOn(Engine.context.NftController, 'updateNftMetadata')
- .mockImplementation(async () => undefined);
-
- const spyOnDetectNfts = jest
- .spyOn(Engine.context.NftDetectionController, 'detectNfts')
- .mockImplementation(async () => undefined);
-
- const { getByTestId } = renderWithProvider(, {
- state: mockState,
- });
- const scrollView = getByTestId('refreshControl');
-
- expect(scrollView).toBeDefined();
-
- const { refreshControl } = scrollView.props;
- await act(async () => {
- await refreshControl.props.onRefresh();
- });
-
- await TestHelpers.delay(1000);
-
- expect(spyOnUpdateNftMetadata).toHaveBeenCalledTimes(0);
- expect(spyOnDetectNfts).toHaveBeenCalledTimes(1);
- });
-
- it('shows spinner if nfts are still being fetched', async () => {
- const CURRENT_ACCOUNT = '0x1a';
- const mockState: DeepPartial = {
- collectibles: {
- favorites: {},
- isNftFetchingProgress: true,
- },
- engine: {
- backgroundState: {
- ...backgroundState,
- NetworkController: {
- ...mockNetworkState({
- chainId: CHAIN_IDS.MAINNET,
- id: 'mainnet',
- nickname: 'Ethereum Mainnet',
- ticker: 'ETH',
- }),
- },
- AccountTrackerController: {
- accounts: { [CURRENT_ACCOUNT]: { balance: '0' } },
- },
- PreferencesController: {
- useNftDetection: true,
- displayNftMedia: true,
- selectedAddress: CURRENT_ACCOUNT,
- identities: {
- [CURRENT_ACCOUNT]: {
- address: CURRENT_ACCOUNT,
- name: 'Account 1',
- },
- },
- },
- NftController: {
- allNfts: {
- [CURRENT_ACCOUNT]: {
- '0x1': [],
- },
- },
- allNftContracts: {
- [CURRENT_ACCOUNT]: {
- '0x1': [],
- },
- },
- },
- },
- },
- };
- const { queryByTestId } = renderWithProvider(, {
- state: mockState,
- });
-
- const spinner = queryByTestId('spinner');
- expect(spinner).not.toBeNull();
- });
-
- it('Does not show spinner if nfts are not still being fetched', async () => {
- const CURRENT_ACCOUNT = '0x1a';
- const mockState: DeepPartial = {
- collectibles: {
- favorites: {},
- },
- engine: {
- backgroundState: {
- ...backgroundState,
- NetworkController: {
- ...mockNetworkState({
- chainId: CHAIN_IDS.MAINNET,
- id: 'mainnet',
- nickname: 'Ethereum Mainnet',
- ticker: 'ETH',
- }),
- },
- AccountTrackerController: {
- accounts: { [CURRENT_ACCOUNT]: { balance: '0' } },
- },
- PreferencesController: {
- useNftDetection: true,
- displayNftMedia: true,
- selectedAddress: CURRENT_ACCOUNT,
- identities: {
- [CURRENT_ACCOUNT]: {
- address: CURRENT_ACCOUNT,
- name: 'Account 1',
- },
- },
- },
- NftController: {
- allNfts: {
- [CURRENT_ACCOUNT]: {
- '0x1': [],
- },
- },
- allNftContracts: {
- [CURRENT_ACCOUNT]: {
- '0x1': [],
- },
- },
- },
- },
- },
- };
-
- const { queryByTestId } = renderWithProvider(, {
- state: mockState,
- });
-
- const spinner = queryByTestId('spinner');
- expect(spinner).toBeNull();
- });
-});
diff --git a/app/components/UI/CollectibleMedia/CollectibleMedia.styles.ts b/app/components/UI/CollectibleMedia/CollectibleMedia.styles.ts
index c8b6470930a..e656d4b6a17 100644
--- a/app/components/UI/CollectibleMedia/CollectibleMedia.styles.ts
+++ b/app/components/UI/CollectibleMedia/CollectibleMedia.styles.ts
@@ -48,6 +48,12 @@ const styleSheet = (params: {
image: {
borderRadius: 12,
},
+ imageHidden: {
+ borderRadius: 12,
+ backgroundColor: colors.background.alternative,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
textContainer: {
alignItems: 'center',
justifyContent: 'flex-start',
diff --git a/app/components/UI/CollectibleMedia/CollectibleMedia.tsx b/app/components/UI/CollectibleMedia/CollectibleMedia.tsx
index e73a5519537..5631db40236 100644
--- a/app/components/UI/CollectibleMedia/CollectibleMedia.tsx
+++ b/app/components/UI/CollectibleMedia/CollectibleMedia.tsx
@@ -18,10 +18,15 @@ import {
ButtonVariants,
} from '../../../component-library/components/Buttons/Button';
import Button from '../../../component-library/components/Buttons/Button/Button';
+import Icon, {
+ IconSize,
+ IconName,
+} from '../../../component-library/components/Icons/Icon';
import { strings } from '../../../../locales/i18n';
import { useNavigation } from '@react-navigation/native';
import Routes from '../../../constants/navigation/Routes';
import { useStyles } from '../../../component-library/hooks';
+import { useTheme } from '../../../util/theme';
const CollectibleMedia: React.FC = ({
collectible,
@@ -35,12 +40,14 @@ const CollectibleMedia: React.FC = ({
onPressColectible,
isTokenImage,
isFullRatio,
+ privacyMode = false,
}) => {
const [sourceUri, setSourceUri] = useState(null);
const isIpfsGatewayEnabled = useSelector(selectIsIpfsGatewayEnabled);
const displayNftMedia = useSelector(selectDisplayNftMedia);
const { navigate } = useNavigation();
+ const { colors } = useTheme();
const { styles } = useStyles(createStyles, {
backgroundColor: collectible.backgroundColor,
});
@@ -161,6 +168,26 @@ const CollectibleMedia: React.FC = ({
);
const renderMedia = useCallback(() => {
+ if (privacyMode) {
+ return (
+
+
+
+ );
+ }
if (
displayNftMedia ||
(!displayNftMedia && isIpfsGatewayEnabled && isIPFSUri(sourceUri))
@@ -223,6 +250,9 @@ const CollectibleMedia: React.FC = ({
styles.tinyImage,
styles.smallImage,
styles.bigImage,
+ styles.imageHidden,
+ colors.text.muted,
+ privacyMode,
cover,
style,
tiny,
diff --git a/app/components/UI/CollectibleMedia/CollectibleMedia.types.ts b/app/components/UI/CollectibleMedia/CollectibleMedia.types.ts
index 760cc9584cf..6132fd8f4b9 100644
--- a/app/components/UI/CollectibleMedia/CollectibleMedia.types.ts
+++ b/app/components/UI/CollectibleMedia/CollectibleMedia.types.ts
@@ -31,4 +31,5 @@ export interface CollectibleMediaProps {
onPressColectible?: () => void;
isTokenImage?: boolean;
isFullRatio?: boolean;
+ privacyMode?: boolean;
}
diff --git a/app/components/UI/CollectibleModal/__snapshots__/CollectibleModal.test.tsx.snap b/app/components/UI/CollectibleModal/__snapshots__/CollectibleModal.test.tsx.snap
index 14dd7d35efd..061f1c7d419 100644
--- a/app/components/UI/CollectibleModal/__snapshots__/CollectibleModal.test.tsx.snap
+++ b/app/components/UI/CollectibleModal/__snapshots__/CollectibleModal.test.tsx.snap
@@ -276,7 +276,7 @@ exports[`CollectibleModal should render correctly 1`] = `
-
+
{strings('transaction.back')}
@@ -660,7 +665,6 @@ export function getOnboardingNavbarOptions(
source={metamask_name}
style={innerStyles.metamaskName}
resizeMethod={'auto'}
-
/>
),
@@ -917,10 +921,9 @@ export function getWalletNavbarOptions(
let formattedAddress = toChecksumHexAddress(selectedInternalAccount.address);
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
- if (isBtcAccount(selectedInternalAccount)) {
- // BTC addresses are not checksummed
- formattedAddress = selectedInternalAccount.address;
- }
+ formattedAddress = getFormattedAddressFromInternalAccount(
+ selectedInternalAccount,
+ );
///: END:ONLY_INCLUDE_IF
const onScanSuccess = (data, content) => {
@@ -1018,6 +1021,18 @@ export function getWalletNavbarOptions(
);
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
+ if (isSolanaAccount(selectedInternalAccount)) {
+ networkPicker = (
+
+ );
+ }
+
if (isBtcAccount(selectedInternalAccount)) {
networkPicker = (
);
}
+
///: END:ONLY_INCLUDE_IF
return {networkPicker};
diff --git a/app/components/UI/NetworkCell/__snapshots__/NetworkCell.test.tsx.snap b/app/components/UI/NetworkCell/__snapshots__/NetworkCell.test.tsx.snap
index c42f8ba6009..28b2fa79f4d 100644
--- a/app/components/UI/NetworkCell/__snapshots__/NetworkCell.test.tsx.snap
+++ b/app/components/UI/NetworkCell/__snapshots__/NetworkCell.test.tsx.snap
@@ -13,7 +13,7 @@ exports[`NetworkCell should render correctly 1`] = `
style={
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 4,
"borderWidth": 0,
"padding": 16,
@@ -123,7 +123,7 @@ exports[`NetworkCell should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -131,7 +131,7 @@ exports[`NetworkCell should render correctly 1`] = `
}
testID="incoming-mainnet-toggle"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
diff --git a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap
index f770473ec63..14432055966 100644
--- a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap
@@ -122,7 +122,7 @@ exports[`NetworkDetails renders correctly 1`] = `
StyleSheet.create({
diff --git a/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap b/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap
index 4c4dababc3e..c3e90cdf813 100644
--- a/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap
+++ b/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap
@@ -144,7 +144,7 @@ exports[`NetworkVerificationInfo renders correctly 1`] = `
+ StyleSheet.create({
+ collectibleIcon: {
+ width: '100%',
+ aspectRatio: 1,
+ },
+ collectibleCard: {
+ flexBasis: '33%',
+ padding: 10,
+ marginBottom: 10,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ footer: {
+ alignItems: 'center',
+ },
+ spinner: {
+ marginBottom: 8,
+ },
+ emptyContainer: {
+ flex: 1,
+ alignItems: 'center',
+ outline: 'solid red 2px',
+ },
+ emptyImageContainer: {
+ width: 30,
+ height: 30,
+ tintColor: colors.background,
+ },
+ headingMd: {
+ marginTop: 10,
+ },
+ emptyText: {
+ color: colors.text.alternative,
+ marginBottom: 8,
+ fontSize: 14,
+ },
+ });
+
+export default styleSheet;
diff --git a/app/components/UI/NftGrid/NftGrid.test.tsx b/app/components/UI/NftGrid/NftGrid.test.tsx
new file mode 100644
index 00000000000..c3e144df1df
--- /dev/null
+++ b/app/components/UI/NftGrid/NftGrid.test.tsx
@@ -0,0 +1,552 @@
+import React from 'react';
+import {
+ render,
+ fireEvent,
+ waitFor,
+ cleanup,
+ act,
+} from '@testing-library/react-native';
+import NftGridFooter from './NftGridFooter';
+import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
+import { StackNavigationProp } from '@react-navigation/stack';
+import NftGridEmpty from './NftGridEmpty';
+import NftGridItem from './NftGridItem';
+import { Nft } from '@metamask/assets-controllers';
+import renderWithProvider, {
+ DeepPartial,
+} from '../../../util/test/renderWithProvider';
+import { RootState } from '../../../reducers';
+import { useNavigation } from '@react-navigation/native';
+import NftGrid from './NftGrid';
+import { mockNetworkState } from '../../../util/test/network';
+import { backgroundState } from '../../../util/test/initial-root-state';
+import { CHAIN_IDS } from '@metamask/transaction-controller';
+import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils';
+// eslint-disable-next-line import/no-namespace
+import * as allSelectors from '../../../../app/reducers/collectibles/index.js';
+import Engine from '../../../core/Engine';
+import TestHelpers from '../../../../e2e/helpers';
+
+jest.mock('../../../core/Engine', () => ({
+ context: {
+ NftController: {
+ addNft: jest.fn(),
+ updateNftMetadata: jest.fn(),
+ checkAndUpdateAllNftsOwnershipStatus: jest.fn(),
+ },
+ NftDetectionController: {
+ detectNfts: jest.fn(),
+ },
+ },
+}));
+
+jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
+ useNavigation: jest.fn(),
+}));
+
+type MockNavigation = StackNavigationProp<
+ {
+ AddAsset: { assetType: string };
+ [key: string]: object | undefined;
+ },
+ 'AddAsset'
+>;
+
+const mockNavigation: MockNavigation = {
+ push: jest.fn(),
+ navigate: jest.fn(),
+ goBack: jest.fn(),
+ pop: jest.fn(),
+ replace: jest.fn(),
+ reset: jest.fn(),
+ popToTop: jest.fn(),
+ isFocused: jest.fn(),
+ canGoBack: jest.fn(),
+ setParams: jest.fn(),
+ getParent: jest.fn(),
+} as unknown as MockNavigation;
+
+const MOCK_ADDRESS = '0xd018538C87232FF95acbCe4870629b75640a78E7';
+const MOCK_CONTRACT_ADDRESS = '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42';
+const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([
+ MOCK_ADDRESS,
+]);
+
+const mockState: DeepPartial = {
+ collectibles: {
+ favorites: {},
+ isNftFetchingProgress: false,
+ },
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ NetworkController: {
+ ...mockNetworkState({
+ chainId: CHAIN_IDS.MAINNET,
+ id: 'mainnet',
+ nickname: 'Ethereum Mainnet',
+ ticker: 'ETH',
+ }),
+ },
+ AccountTrackerController: {
+ accounts: { [MOCK_ADDRESS]: { balance: '0' } },
+ },
+ PreferencesController: {
+ useNftDetection: true,
+ displayNftMedia: true,
+ },
+ AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
+ NftController: {
+ allNfts: {
+ [MOCK_ADDRESS.toLowerCase()]: {
+ [CHAIN_IDS.MAINNET]: [
+ {
+ address: MOCK_CONTRACT_ADDRESS,
+ description:
+ 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
+ favorite: false,
+ image: 'https://api.pudgypenguins.io/lil/image/113',
+ isCurrentlyOwned: true,
+ name: 'Lil Pudgy #113',
+ standard: 'ERC721',
+ tokenId: '113',
+ tokenURI: 'https://api.pudgypenguins.io/lil/113',
+ },
+ {
+ address: MOCK_CONTRACT_ADDRESS,
+ description:
+ 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
+ favorite: false,
+ image: 'https://api.pudgypenguins.io/lil/image/113',
+ isCurrentlyOwned: false,
+ name: 'Lil Pudgy #114',
+ standard: 'ERC721',
+ tokenId: '114',
+ tokenURI: 'https://api.pudgypenguins.io/lil/114',
+ },
+ ],
+ },
+ },
+ allNftContracts: {
+ [MOCK_ADDRESS]: {
+ [CHAIN_IDS.MAINNET]: [
+ {
+ address: MOCK_CONTRACT_ADDRESS,
+ name: 'MyToken',
+ symbol: 'MTK',
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+};
+
+const nftItemData = [
+ {
+ address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
+ description:
+ 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
+ error: 'Opensea import error',
+ favorite: false,
+ image: 'https://api.pudgypenguins.io/lil/image/11222',
+ isCurrentlyOwned: true,
+ name: 'Lil Pudgy #113',
+ standard: 'ERC721',
+ tokenId: '113',
+ tokenURI: 'https://api.pudgypenguins.io/lil/113',
+ },
+];
+
+const nftItemDataUpdated = [
+ {
+ address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
+ description:
+ 'Lil Pudgys are a collection of 22,222 randomly generated NFTs minted on Ethereum.',
+ error: 'Opensea import error',
+ favorite: false,
+ image: 'https://api.pudgypenguins.io/lil/image/updated.png',
+ isCurrentlyOwned: true,
+ name: 'Lil Pudgy #113',
+ standard: 'ERC721',
+ tokenId: '113',
+ tokenURI: 'https://api.pudgypenguins.io/lil/113',
+ },
+];
+
+const collectibleData = [
+ {
+ address: '0x72b1FDb6443338A158DeC2FbF411B71aeB157A42',
+ name: 'MyToken',
+ symbol: 'MTK',
+ },
+];
+
+describe('NftGrid', () => {
+ afterEach(cleanup);
+ const mockNavigate = jest.fn();
+ (useNavigation as jest.Mock).mockReturnValue({
+ navigate: mockNavigate,
+ goBack: jest.fn(),
+ push: jest.fn(),
+ replace: jest.fn(),
+ reset: jest.fn(),
+ popToTop: jest.fn(),
+ });
+
+ it('should only get owned collectibles', () => {
+ const { queryByTestId } = renderWithProvider(
+ ,
+ {
+ state: mockState,
+ },
+ );
+
+ const ownedNft = queryByTestId('Lil Pudgy #113');
+ const nonOwnedNft = queryByTestId('Lil Pudgy #114');
+
+ expect(ownedNft).toBeTruthy();
+ expect(nonOwnedNft).toBeNull();
+ });
+
+ it('UI refresh changes NFT image when metadata image changes - detection disabled', async () => {
+ const testState: DeepPartial = {
+ ...mockState,
+ engine: {
+ ...mockState.engine,
+ backgroundState: {
+ ...mockState?.engine?.backgroundState,
+ PreferencesController: {
+ ...mockState?.engine?.backgroundState?.PreferencesController,
+ useNftDetection: false,
+ },
+ },
+ },
+ };
+
+ const spyOnCollectibles = jest
+ .spyOn(allSelectors, 'collectiblesSelector')
+ .mockReturnValue(nftItemData);
+
+ const spyOnUpdateNftMetadata = jest
+ .spyOn(Engine.context.NftController, 'updateNftMetadata')
+ .mockImplementation(async () => undefined);
+
+ const { getByTestId: getByTestIdBefore } = renderWithProvider(
+ ,
+ {
+ state: testState,
+ },
+ );
+
+ jest.clearAllMocks();
+ jest
+ .spyOn(allSelectors, 'collectiblesSelector')
+ .mockReturnValue(nftItemDataUpdated);
+
+ const nftImageBefore = getByTestIdBefore('nft-image');
+ expect(nftImageBefore.props.source.uri).toEqual(nftItemData[0].image);
+
+ const { getByTestId: getByTestIdAfter } = renderWithProvider(
+ ,
+ {
+ state: testState,
+ },
+ );
+
+ await waitFor(() => {
+ // ensure only one call, and with the updated required updated metadata
+ expect(spyOnUpdateNftMetadata).toHaveBeenCalledTimes(1);
+ expect(spyOnUpdateNftMetadata).toHaveBeenCalledWith({
+ nfts: nftItemDataUpdated,
+ userAddress: MOCK_ADDRESS,
+ });
+ const nftImageAfter = getByTestIdAfter('nft-image');
+ expect(nftImageAfter.props.source.uri).toEqual(
+ nftItemDataUpdated[0].image,
+ );
+ });
+
+ spyOnCollectibles.mockRestore();
+ spyOnUpdateNftMetadata.mockRestore();
+ });
+
+ it('UI refresh changes NFT image when metadata image changes - detection enabled', async () => {
+ const testState: DeepPartial = {
+ ...mockState,
+ engine: {
+ ...mockState.engine,
+ backgroundState: {
+ ...mockState?.engine?.backgroundState,
+ PreferencesController: {
+ ...mockState?.engine?.backgroundState?.PreferencesController,
+ useNftDetection: true, // Override useNftDetection here
+ },
+ },
+ },
+ };
+
+ const spyOnCollectibles = jest
+ .spyOn(allSelectors, 'collectiblesSelector')
+ .mockReturnValue(nftItemData);
+
+ const spyOnUpdateNftMetadata = jest
+ .spyOn(Engine.context.NftController, 'updateNftMetadata')
+ .mockImplementation(async () => undefined);
+
+ const { getByTestId } = renderWithProvider(
+ ,
+ {
+ state: testState,
+ },
+ );
+
+ jest.clearAllMocks();
+ jest
+ .spyOn(allSelectors, 'collectiblesSelector')
+ .mockReturnValue(nftItemDataUpdated);
+
+ const nftImageBefore = getByTestId('nft-image');
+ expect(nftImageBefore.props.source.uri).toEqual(nftItemData[0].image);
+
+ const { getByTestId: getAfterByTestId } = renderWithProvider(
+ ,
+ {
+ state: testState,
+ },
+ );
+
+ await waitFor(() => {
+ expect(spyOnUpdateNftMetadata).toHaveBeenCalledTimes(0);
+ const nftImageAfter = getAfterByTestId('nft-image');
+ expect(nftImageAfter.props.source.uri).toEqual(
+ nftItemDataUpdated[0].image,
+ );
+ });
+
+ spyOnCollectibles.mockRestore();
+ spyOnUpdateNftMetadata.mockRestore();
+ });
+
+ it('UI pull down experience should call detectNfts when detection is enabled', async () => {
+ const testState: DeepPartial = {
+ ...mockState,
+ engine: {
+ ...mockState.engine,
+ backgroundState: {
+ ...mockState?.engine?.backgroundState,
+ PreferencesController: {
+ ...mockState?.engine?.backgroundState?.PreferencesController,
+ useNftDetection: true, // Override useNftDetection here
+ },
+ },
+ },
+ };
+
+ jest
+ .spyOn(allSelectors, 'collectiblesSelector')
+ .mockReturnValueOnce(nftItemData)
+ .mockReturnValueOnce(nftItemDataUpdated);
+ jest
+ .spyOn(allSelectors, 'collectibleContractsSelector')
+ .mockReturnValue(collectibleData);
+ const spyOnUpdateNftMetadata = jest
+ .spyOn(Engine.context.NftController, 'updateNftMetadata')
+ .mockImplementation(async () => undefined);
+
+ const spyOnDetectNfts = jest
+ .spyOn(Engine.context.NftDetectionController, 'detectNfts')
+ .mockImplementation(async () => undefined);
+
+ const { getByTestId } = renderWithProvider(
+ ,
+ {
+ state: testState,
+ },
+ );
+ const scrollView = getByTestId('refreshControl');
+
+ expect(scrollView).toBeDefined();
+
+ const { refreshControl } = scrollView.props;
+ await act(async () => {
+ await refreshControl.props.onRefresh();
+ });
+
+ await TestHelpers.delay(1000);
+
+ expect(spyOnUpdateNftMetadata).toHaveBeenCalledTimes(0);
+ expect(spyOnDetectNfts).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows spinner if nfts are still being fetched', async () => {
+ const testState: DeepPartial = {
+ ...mockState,
+ collectibles: {
+ ...mockState.collectibles,
+ favorites: {},
+ isNftFetchingProgress: true,
+ },
+ engine: {
+ ...mockState.engine,
+ backgroundState: {
+ ...mockState?.engine?.backgroundState,
+ PreferencesController: {
+ ...mockState?.engine?.backgroundState?.PreferencesController,
+ useNftDetection: true, // Override useNftDetection here
+ },
+ },
+ },
+ };
+
+ const { queryByTestId } = renderWithProvider(
+ ,
+ {
+ state: testState,
+ },
+ );
+
+ const spinner = queryByTestId('spinner');
+ expect(spinner).not.toBeNull();
+ });
+
+ it('Does not show spinner if nfts are not still being fetched', async () => {
+ const testState: DeepPartial = {
+ collectibles: {
+ favorites: {},
+ },
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ NetworkController: {
+ ...mockNetworkState({
+ chainId: CHAIN_IDS.MAINNET,
+ id: 'mainnet',
+ nickname: 'Ethereum Mainnet',
+ ticker: 'ETH',
+ }),
+ },
+ AccountTrackerController: {
+ accounts: { [MOCK_ADDRESS.toLowerCase()]: { balance: '0' } },
+ },
+ PreferencesController: {
+ useNftDetection: true,
+ displayNftMedia: true,
+ selectedAddress: MOCK_ADDRESS.toLowerCase(),
+ identities: {
+ [MOCK_ADDRESS.toLowerCase()]: {
+ address: MOCK_ADDRESS.toLowerCase(),
+ name: 'Account 1',
+ },
+ },
+ },
+ NftController: {
+ allNfts: {
+ [MOCK_ADDRESS.toLowerCase()]: {
+ '0x1': [],
+ },
+ },
+ allNftContracts: {
+ [MOCK_ADDRESS.toLowerCase()]: {
+ '0x1': [],
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const { queryByTestId } = renderWithProvider(
+ ,
+ {
+ state: testState,
+ },
+ );
+
+ const spinner = queryByTestId('spinner');
+ expect(spinner).toBeNull();
+ });
+});
+
+describe('NftGridFooter', () => {
+ it('renders without crashing', () => {
+ const { getByText } = render();
+ expect(getByText('Don’t see your NFT?')).toBeTruthy();
+ expect(getByText('Import NFTs')).toBeTruthy();
+ });
+
+ it('calls navigation.push when the button is pressed', () => {
+ const { getByTestId } = render(
+ ,
+ );
+ const button = getByTestId(WalletViewSelectorsIDs.IMPORT_NFT_BUTTON);
+ fireEvent.press(button);
+ expect(mockNavigation.push).toHaveBeenCalledWith('AddAsset', {
+ assetType: 'collectible',
+ });
+ });
+});
+
+describe('NftGridEmpty', () => {
+ it('renders without crashing', () => {
+ const { getByText } = render();
+ expect(getByText('No NFTs yet')).toBeTruthy();
+ expect(getByText('Learn more')).toBeTruthy();
+ });
+
+ it('calls navigation.navigate when the button is pressed', () => {
+ const { getByText } = render();
+ const learnMoreText = getByText('Learn more');
+ fireEvent.press(learnMoreText);
+
+ // TODO: actually test for learn more redirect
+ expect(mockNavigation.navigate).toHaveBeenCalledWith('Webview', {
+ screen: 'SimpleWebview',
+ params: {
+ url: 'https://support.metamask.io/nfts/nft-tokens-in-your-metamask-wallet/',
+ },
+ });
+ });
+});
+
+describe('NftGridItem', () => {
+ const mockNft: Nft = {
+ address: '0x123',
+ tokenId: '1',
+ name: 'Test NFT',
+ image: 'https://example.com/image.png',
+ collection: {
+ name: 'Test Collection',
+ },
+ description: '',
+ standard: 'erc721',
+ };
+
+ const mockNavigate = jest.fn();
+ (useNavigation as jest.Mock).mockReturnValue({
+ navigate: mockNavigate,
+ goBack: jest.fn(),
+ push: jest.fn(),
+ replace: jest.fn(),
+ reset: jest.fn(),
+ popToTop: jest.fn(),
+ });
+
+ it('renders correctly with a valid nft', () => {
+ const { getByText, getByTestId } = renderWithProvider(
+ ,
+ { state: mockState },
+ );
+
+ expect(getByTestId(mockNft.name as string)).toBeTruthy();
+ expect(getByText('Test NFT')).toBeTruthy();
+ expect(getByText('Test Collection')).toBeTruthy();
+ });
+});
diff --git a/app/components/UI/NftGrid/NftGrid.tsx b/app/components/UI/NftGrid/NftGrid.tsx
new file mode 100644
index 00000000000..55b3e9391cf
--- /dev/null
+++ b/app/components/UI/NftGrid/NftGrid.tsx
@@ -0,0 +1,288 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import {
+ View,
+ Alert,
+ ActivityIndicator,
+ FlatList,
+ RefreshControl,
+} from 'react-native';
+import { useSelector } from 'react-redux';
+import ActionSheet from '@metamask/react-native-actionsheet';
+import { strings } from '../../../../locales/i18n';
+import Engine from '../../../core/Engine';
+import { removeFavoriteCollectible } from '../../../actions/collectibles';
+import {
+ collectiblesSelector,
+ isNftFetchingProgressSelector,
+} from '../../../reducers/collectibles';
+import { useTheme } from '../../../util/theme';
+import {
+ MetaMetricsEvents,
+ useMetrics,
+} from '../../../components/hooks/useMetrics';
+import { getDecimalChainId } from '../../../util/networks';
+import { Nft } from '@metamask/assets-controllers';
+import styleSheet from './NftGrid.styles';
+import { StackNavigationProp } from '@react-navigation/stack';
+import CollectibleDetectionModal from '../CollectibleDetectionModal';
+import {
+ selectDisplayNftMedia,
+ selectIsIpfsGatewayEnabled,
+ selectPrivacyMode,
+ selectUseNftDetection,
+} from '../../../selectors/preferencesController';
+import NftGridItem from './NftGridItem';
+import NftGridEmpty from './NftGridEmpty';
+import NftGridFooter from './NftGridFooter';
+import { useNavigation } from '@react-navigation/native';
+
+export const RefreshTestId = 'refreshControl';
+export const SpinnerTestId = 'spinner';
+
+interface ActionSheetType {
+ show: () => void;
+}
+
+export interface LongPressedCollectibleType {
+ address: string;
+ tokenId: string;
+}
+
+interface NftGridNavigationParamList {
+ AddAsset: { assetType: string };
+ [key: string]: undefined | object;
+}
+
+interface NftGridProps {
+ chainId: string;
+ selectedAddress: string;
+}
+
+function NftGrid({ chainId, selectedAddress }: NftGridProps) {
+ const navigation =
+ useNavigation<
+ StackNavigationProp
+ >();
+ const collectibles = useSelector(collectiblesSelector).filter(
+ (singleCollectible: Nft) => singleCollectible.isCurrentlyOwned === true,
+ );
+ const privacyMode = useSelector(selectPrivacyMode);
+ const isIpfsGatewayEnabled = useSelector(selectIsIpfsGatewayEnabled);
+ const displayNftMedia = useSelector(selectDisplayNftMedia);
+ const useNftDetection = useSelector(selectUseNftDetection);
+ const isNftFetchingProgress = useSelector(isNftFetchingProgressSelector);
+ const isNftDetectionEnabled = useSelector(selectUseNftDetection);
+ const actionSheetRef = useRef(null);
+ const longPressedCollectible = useRef(
+ null,
+ );
+ const { themeAppearance, colors } = useTheme();
+ const styles = styleSheet(colors);
+ const { trackEvent, createEventBuilder } = useMetrics();
+
+ const [refreshing, setRefreshing] = useState(false);
+
+ const onRefresh = useCallback(async () => {
+ requestAnimationFrame(async () => {
+ setRefreshing(true);
+ const { NftDetectionController, NftController } = Engine.context;
+ const actions = [
+ NftDetectionController.detectNfts(),
+ NftController.checkAndUpdateAllNftsOwnershipStatus(),
+ ];
+ await Promise.allSettled(actions);
+ setRefreshing(false);
+ });
+ }, [setRefreshing]);
+
+ const removeNft = () => {
+ const { NftController } = Engine.context;
+
+ if (
+ !longPressedCollectible?.current?.address &&
+ !longPressedCollectible?.current?.tokenId
+ ) {
+ return null;
+ }
+
+ removeFavoriteCollectible(
+ selectedAddress,
+ chainId,
+ longPressedCollectible.current,
+ );
+ NftController.removeAndIgnoreNft(
+ longPressedCollectible.current.address,
+ longPressedCollectible.current.tokenId,
+ );
+
+ trackEvent(
+ createEventBuilder(MetaMetricsEvents.COLLECTIBLE_REMOVED)
+ .addProperties({
+ chain_id: getDecimalChainId(chainId),
+ })
+ .build(),
+ );
+ Alert.alert(
+ strings('wallet.collectible_removed_title'),
+ strings('wallet.collectible_removed_desc'),
+ );
+ };
+
+ const refreshMetadata = () => {
+ const { NftController } = Engine.context;
+
+ if (
+ !longPressedCollectible?.current?.address &&
+ !longPressedCollectible?.current?.tokenId
+ ) {
+ return null;
+ }
+
+ NftController.addNft(
+ longPressedCollectible.current.address,
+ longPressedCollectible.current.tokenId,
+ );
+ };
+
+ const FIRST_MENU_ACTION = 0;
+ const SECOND_MENU_ACTION = 1;
+
+ const handleMenuAction = (index: number) => {
+ if (index === FIRST_MENU_ACTION) {
+ refreshMetadata();
+ } else if (index === SECOND_MENU_ACTION) {
+ removeNft();
+ }
+ };
+
+ const isCollectibleIgnored = useCallback(
+ (collectible) => {
+ const found = collectibles.find(
+ (elm: Nft) =>
+ elm.address === collectible.address &&
+ elm.tokenId === collectible.tokenId,
+ );
+ if (found) return false;
+ return true;
+ },
+ [collectibles],
+ );
+
+ const shouldUpdateCollectibleMetadata = (collectible: Nft) =>
+ typeof collectible.tokenId === 'number' ||
+ (typeof collectible.tokenId === 'string' &&
+ !Number.isNaN(Number(collectible.tokenId)));
+
+ const updateAllCollectibleMetadata = useCallback(
+ async (collectiblesArr: Nft[]) => {
+ const { NftController } = Engine.context;
+ // Filter out ignored collectibles
+ const filteredcollectibles = collectiblesArr.filter(
+ (collectible: Nft) => !isCollectibleIgnored(collectible),
+ );
+
+ // filter removable collectible
+ const removable = filteredcollectibles.filter((single: Nft) =>
+ String(single.tokenId).includes('e+'),
+ );
+ const updatable = filteredcollectibles.filter(
+ (single: Nft) => !String(single.tokenId).includes('e+'),
+ );
+
+ removable.forEach((elm: Nft) => {
+ removeFavoriteCollectible(selectedAddress, chainId, elm);
+ });
+
+ if (updatable.length !== 0) {
+ await NftController.updateNftMetadata({
+ nfts: updatable,
+ userAddress: selectedAddress,
+ });
+ }
+ },
+ [isCollectibleIgnored, chainId, selectedAddress],
+ );
+
+ useEffect(() => {
+ if (!isIpfsGatewayEnabled && !displayNftMedia) {
+ return;
+ }
+ // TO DO: Move this fix to the controllers layer
+ const updatableCollectibles = collectibles.filter((single: Nft) =>
+ shouldUpdateCollectibleMetadata(single),
+ );
+ if (updatableCollectibles.length !== 0 && !useNftDetection) {
+ updateAllCollectibleMetadata(updatableCollectibles);
+ }
+ }, [
+ collectibles,
+ updateAllCollectibleMetadata,
+ isIpfsGatewayEnabled,
+ displayNftMedia,
+ useNftDetection,
+ ]);
+
+ return (
+
+ {!isNftDetectionEnabled && }
+ {/* fetching state */}
+ {isNftFetchingProgress && (
+
+ )}
+ {/* empty state */}
+ {!isNftFetchingProgress && collectibles.length === 0 && (
+ <>
+
+
+ >
+ )}
+ {/* nft grid */}
+ {!isNftFetchingProgress && collectibles.length > 0 && (
+ (
+
+ )}
+ keyExtractor={(_, index) => index.toString()}
+ testID={RefreshTestId}
+ refreshControl={
+
+ }
+ ListFooterComponent={}
+ />
+ )}
+
+
+ );
+}
+
+export default NftGrid;
diff --git a/app/components/UI/NftGrid/NftGridEmpty.tsx b/app/components/UI/NftGrid/NftGridEmpty.tsx
new file mode 100644
index 00000000000..d4ccf2db306
--- /dev/null
+++ b/app/components/UI/NftGrid/NftGridEmpty.tsx
@@ -0,0 +1,83 @@
+import React, { useCallback } from 'react';
+import { View, TouchableOpacity, Image } from 'react-native';
+import { strings } from '../../../../locales/i18n';
+import Text, {
+ TextColor,
+ TextVariant,
+} from '../../../component-library/components/Texts/Text';
+import styleSheet from './NftGrid.styles';
+import { StackNavigationProp } from '@react-navigation/stack';
+import AppConstants from '../../../core/AppConstants';
+
+import noNftPlaceholderSrc from '../../../images/no-nfts-placeholder.png';
+import { useTheme } from '../../../util/theme';
+interface NftGridNavigationParamList {
+ AddAsset: { assetType: string };
+ [key: string]: undefined | object;
+}
+
+interface NftGridProps {
+ navigation: StackNavigationProp;
+}
+
+function NftGridEmpty({ navigation }: NftGridProps) {
+ const { colors } = useTheme();
+ const styles = styleSheet(colors);
+
+ const goToLearnMore = useCallback(
+ () =>
+ navigation.navigate('Webview', {
+ screen: 'SimpleWebview',
+ params: { url: AppConstants.URLS.NFT },
+ }),
+ [navigation],
+ );
+
+ return (
+
+
+
+ {strings('wallet.no_nfts_yet')}
+
+
+ navigation.navigate('Webview', {
+ screen: 'SimpleWebview',
+ params: { url: AppConstants.URLS.NFT },
+ })
+ }
+ >
+
+ {strings('wallet.learn_more')}
+
+
+
+ );
+}
+
+export default NftGridEmpty;
diff --git a/app/components/UI/NftGrid/NftGridFooter.tsx b/app/components/UI/NftGrid/NftGridFooter.tsx
new file mode 100644
index 00000000000..1668a6a35fe
--- /dev/null
+++ b/app/components/UI/NftGrid/NftGridFooter.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { View, TouchableOpacity } from 'react-native';
+import { strings } from '../../../../locales/i18n';
+import Text, {
+ TextColor,
+ TextVariant,
+} from '../../../component-library/components/Texts/Text';
+import { StackNavigationProp } from '@react-navigation/stack';
+import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
+
+interface NftGridNavigationParamList {
+ AddAsset: { assetType: string };
+ [key: string]: undefined | object;
+}
+
+interface NftGridFooterProps {
+ navigation: StackNavigationProp;
+}
+
+function NftGridFooter({ navigation }: NftGridFooterProps) {
+ return (
+
+
+ {strings('wallet.no_collectibles')}
+
+
+ navigation.push('AddAsset', { assetType: 'collectible' })
+ }
+ disabled={false}
+ testID={WalletViewSelectorsIDs.IMPORT_NFT_BUTTON}
+ >
+
+ {strings('wallet.add_collectibles')}
+
+
+
+ );
+}
+
+export default NftGridFooter;
diff --git a/app/components/UI/NftGrid/NftGridItem.tsx b/app/components/UI/NftGrid/NftGridItem.tsx
new file mode 100644
index 00000000000..1c3e8d280c1
--- /dev/null
+++ b/app/components/UI/NftGrid/NftGridItem.tsx
@@ -0,0 +1,82 @@
+import React, { MutableRefObject, RefObject, useCallback } from 'react';
+import { TouchableOpacity } from 'react-native';
+import CollectibleMedia from '../CollectibleMedia';
+import Text from '../../../component-library/components/Texts/Text';
+import { Nft } from '@metamask/assets-controllers';
+import { debounce } from 'lodash';
+import styleSheet from './NftGrid.styles';
+import { StackNavigationProp } from '@react-navigation/stack';
+import { useTheme } from '../../../util/theme';
+import { LongPressedCollectibleType } from './NftGrid';
+
+const debouncedNavigation = debounce((navigation, collectible) => {
+ navigation.navigate('NftDetails', { collectible });
+}, 200);
+
+interface ActionSheetType {
+ show: () => void;
+}
+
+interface NftGridNavigationParamList {
+ AddAsset: { assetType: string };
+ [key: string]: undefined | object;
+}
+
+function NftGridItem({
+ nft,
+ navigation,
+ privacyMode = false,
+ actionSheetRef,
+ longPressedCollectible,
+}: {
+ nft: Nft;
+ navigation: StackNavigationProp;
+ privacyMode?: boolean;
+ actionSheetRef: RefObject;
+ longPressedCollectible: MutableRefObject;
+}) {
+ const { colors } = useTheme();
+ const styles = styleSheet(colors);
+
+ const onLongPressCollectible = useCallback(
+ (collectible) => {
+ actionSheetRef?.current?.show();
+ longPressedCollectible.current = collectible;
+ },
+ [actionSheetRef, longPressedCollectible],
+ );
+
+ const onItemPress = useCallback(
+ (nftItem) => {
+ debouncedNavigation(navigation, nftItem);
+ },
+ [navigation],
+ );
+
+ if (!nft) return null;
+
+ return (
+ onItemPress(nft)}
+ onLongPress={() => onLongPressCollectible(nft)}
+ testID={nft.name as string}
+ >
+
+
+ {nft.name}
+
+
+ {nft.collection?.name}
+
+
+ );
+}
+
+export default NftGridItem;
diff --git a/app/components/UI/NftGrid/index.tsx b/app/components/UI/NftGrid/index.tsx
new file mode 100644
index 00000000000..014cac6b75b
--- /dev/null
+++ b/app/components/UI/NftGrid/index.tsx
@@ -0,0 +1 @@
+export { default } from './NftGrid';
diff --git a/app/components/UI/Notification/BaseNotification/__snapshots__/index.test.jsx.snap b/app/components/UI/Notification/BaseNotification/__snapshots__/index.test.jsx.snap
index fc9fc68fc42..8a4fef419be 100644
--- a/app/components/UI/Notification/BaseNotification/__snapshots__/index.test.jsx.snap
+++ b/app/components/UI/Notification/BaseNotification/__snapshots__/index.test.jsx.snap
@@ -21,7 +21,7 @@ exports[`BaseNotification gets icon correctly for each status 1`] = `
activeOpacity={0.8}
style={
{
- "backgroundColor": "#000000cc",
+ "backgroundColor": "#000000CC",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
@@ -216,7 +216,7 @@ exports[`BaseNotification gets icon correctly for each status 2`] = `
activeOpacity={0.8}
style={
{
- "backgroundColor": "#000000cc",
+ "backgroundColor": "#000000CC",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
@@ -411,7 +411,7 @@ exports[`BaseNotification gets icon correctly for each status 3`] = `
activeOpacity={0.8}
style={
{
- "backgroundColor": "#000000cc",
+ "backgroundColor": "#000000CC",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
@@ -606,7 +606,7 @@ exports[`BaseNotification gets icon correctly for each status 4`] = `
activeOpacity={0.8}
style={
{
- "backgroundColor": "#000000cc",
+ "backgroundColor": "#000000CC",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
@@ -771,7 +771,7 @@ exports[`BaseNotification gets icon correctly for each status 5`] = `
activeOpacity={0.8}
style={
{
- "backgroundColor": "#000000cc",
+ "backgroundColor": "#000000CC",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
@@ -936,7 +936,7 @@ exports[`BaseNotification gets icon correctly for each status 6`] = `
activeOpacity={0.8}
style={
{
- "backgroundColor": "#000000cc",
+ "backgroundColor": "#000000CC",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
@@ -1101,7 +1101,7 @@ exports[`BaseNotification gets icon correctly for each status 7`] = `
activeOpacity={0.8}
style={
{
- "backgroundColor": "#000000cc",
+ "backgroundColor": "#000000CC",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
@@ -1266,7 +1266,7 @@ exports[`BaseNotification gets icon correctly for each status 8`] = `
activeOpacity={0.8}
style={
{
- "backgroundColor": "#000000cc",
+ "backgroundColor": "#000000CC",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
@@ -1431,7 +1431,7 @@ exports[`BaseNotification gets icon correctly for each status 9`] = `
activeOpacity={0.8}
style={
{
- "backgroundColor": "#000000cc",
+ "backgroundColor": "#000000CC",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
@@ -1596,7 +1596,7 @@ exports[`BaseNotification gets icon correctly for each status 10`] = `
activeOpacity={0.8}
style={
{
- "backgroundColor": "#000000cc",
+ "backgroundColor": "#000000CC",
"borderRadius": 8,
"flex": 1,
"flexDirection": "row",
diff --git a/app/components/UI/Notification/Modal/styles.ts b/app/components/UI/Notification/Modal/styles.ts
index 5234cc83fa5..e79ca6efa19 100644
--- a/app/components/UI/Notification/Modal/styles.ts
+++ b/app/components/UI/Notification/Modal/styles.ts
@@ -1,7 +1,6 @@
// Third party dependencies.
import { StyleSheet, TextStyle } from 'react-native';
-import { typography } from '@metamask/design-tokens';
-import { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types';
+import { typography, ThemeColors } from '@metamask/design-tokens';
/**
* Style sheet function for AmbiguousAddressSheet component.
diff --git a/app/components/UI/Notification/ResetNotificationsModal/__snapshots__/ResetNotificationsModal.test.tsx.snap b/app/components/UI/Notification/ResetNotificationsModal/__snapshots__/ResetNotificationsModal.test.tsx.snap
index ed08cd71f96..46e093abdfe 100644
--- a/app/components/UI/Notification/ResetNotificationsModal/__snapshots__/ResetNotificationsModal.test.tsx.snap
+++ b/app/components/UI/Notification/ResetNotificationsModal/__snapshots__/ResetNotificationsModal.test.tsx.snap
@@ -70,14 +70,14 @@ exports[`ProfileSyncingModal should render correctly 1`] = `
[
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
"borderWidth": 1,
"maxHeight": 1334,
"overflow": "hidden",
"paddingBottom": 0,
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -107,7 +107,7 @@ exports[`ProfileSyncingModal should render correctly 1`] = `
StyleSheet.create({
diff --git a/app/components/UI/OptinMetrics/index.js b/app/components/UI/OptinMetrics/index.js
index 31a7812f9c8..8c2c03ec0a1 100644
--- a/app/components/UI/OptinMetrics/index.js
+++ b/app/components/UI/OptinMetrics/index.js
@@ -368,36 +368,35 @@ class OptinMetrics extends PureComponent {
setDataCollectionForMarketing(false);
}
- InteractionManager.runAfterInteractions(async () => {
- // consolidate device and user settings traits
- const consolidatedTraits = {
- ...dataCollectionForMarketingTraits,
- is_metrics_opted_in: true,
- ...generateDeviceAnalyticsMetaData(),
- ...generateUserSettingsAnalyticsMetaData(),
- };
- await metrics.addTraitsToUser(consolidatedTraits);
-
- // track onboarding events that were stored before user opted in
- // only if the user eventually opts in.
- if (events && events.length) {
- let delay = 0; // Initialize delay
- const eventTrackingDelay = 200; // ms delay between each event
- events.forEach((eventArgs) => {
- // delay each event to prevent them from
- // being tracked with the same timestamp
- // which would cause them to be grouped together
- // by sentAt time in the Segment dashboard
- // as precision is only to the milisecond
- // and loop seems to runs faster than that
- setTimeout(() => {
- metrics.trackEvent(...eventArgs);
- }, delay);
- delay += eventTrackingDelay;
- });
- }
- this.props.clearOnboardingEvents();
- });
+ // consolidate device and user settings traits
+ const consolidatedTraits = {
+ ...dataCollectionForMarketingTraits,
+ is_metrics_opted_in: true,
+ ...generateDeviceAnalyticsMetaData(),
+ ...generateUserSettingsAnalyticsMetaData(),
+ };
+ await metrics.addTraitsToUser(consolidatedTraits);
+
+ // track onboarding events that were stored before user opted in
+ // only if the user eventually opts in.
+ if (events && events.length) {
+ let delay = 0; // Initialize delay
+ const eventTrackingDelay = 200; // ms delay between each event
+ events.forEach((eventArgs) => {
+ // delay each event to prevent them from
+ // being tracked with the same timestamp
+ // which would cause them to be grouped together
+ // by sentAt time in the Segment dashboard
+ // as precision is only to the milisecond
+ // and loop seems to runs faster than that
+ setTimeout(() => {
+ metrics.trackEvent(...eventArgs);
+ }, delay);
+ delay += eventTrackingDelay;
+ });
+ }
+ this.props.clearOnboardingEvents();
+
this.continue();
};
diff --git a/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap b/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap
index 9db1d0309f3..04d04cbf14b 100644
--- a/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap
@@ -71,7 +71,7 @@ exports[`PaymentRequest renders correctly 1`] = `
style={
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"flexDirection": "row",
@@ -177,7 +177,7 @@ exports[`PaymentRequest renders correctly 1`] = `
"borderWidth": 1,
},
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"marginBottom": 8,
@@ -326,7 +326,7 @@ exports[`PaymentRequest renders correctly 1`] = `
"borderWidth": 1,
},
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"marginBottom": 8,
diff --git a/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap b/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap
index 606816b874f..67f8a5036e4 100644
--- a/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap
+++ b/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap
@@ -157,7 +157,7 @@ exports[`PermissionsSummary should render correctly 1`] = `
{
"alignItems": "center",
"alignSelf": "flex-start",
- "backgroundColor": "#0000001a",
+ "backgroundColor": "#0000001A",
"borderRadius": 16,
"height": 32,
"justifyContent": "center",
@@ -441,7 +441,7 @@ exports[`PermissionsSummary should render correctly 1`] = `
{
"alignItems": "center",
"alignSelf": "flex-start",
- "backgroundColor": "#0000001a",
+ "backgroundColor": "#0000001A",
"borderRadius": 16,
"height": 32,
"justifyContent": "center",
@@ -937,7 +937,7 @@ exports[`PermissionsSummary should render correctly for network switch 1`] = `
{
"alignItems": "center",
"alignSelf": "flex-start",
- "backgroundColor": "#0000001a",
+ "backgroundColor": "#0000001A",
"borderRadius": 16,
"height": 32,
"justifyContent": "center",
diff --git a/app/components/UI/ProfileSyncing/ProfileSyncingModal/__snapshots__/ProfileSyncingModal.test.tsx.snap b/app/components/UI/ProfileSyncing/ProfileSyncingModal/__snapshots__/ProfileSyncingModal.test.tsx.snap
index ee24fee702a..e98efc084f3 100644
--- a/app/components/UI/ProfileSyncing/ProfileSyncingModal/__snapshots__/ProfileSyncingModal.test.tsx.snap
+++ b/app/components/UI/ProfileSyncing/ProfileSyncingModal/__snapshots__/ProfileSyncingModal.test.tsx.snap
@@ -70,14 +70,14 @@ exports[`ProfileSyncingModal should render correctly 1`] = `
[
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
"borderWidth": 1,
"maxHeight": 1334,
"overflow": "hidden",
"paddingBottom": 0,
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -107,7 +107,7 @@ exports[`ProfileSyncingModal should render correctly 1`] = `
@@ -908,13 +908,13 @@ exports[`Settings Activation Keys renders correctly when is loading 1`] = `
"width": 51,
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
@@ -4269,13 +4269,13 @@ exports[`Settings renders correctly for internal builds 1`] = `
"width": 51,
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -4488,13 +4488,13 @@ exports[`Settings renders correctly for internal builds 1`] = `
"width": 51,
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/UI/Ramp/constants/index.ts b/app/components/UI/Ramp/constants/index.ts
index 0c2efd8a9f6..d2f0f854cb6 100644
--- a/app/components/UI/Ramp/constants/index.ts
+++ b/app/components/UI/Ramp/constants/index.ts
@@ -1,5 +1 @@
-import { AnalyticsEvents } from '../types';
-
-export const AnonymousEvents: (keyof AnalyticsEvents)[] = [];
-
export const RAMPS_SEND = 'RAMPS_SEND';
diff --git a/app/components/UI/Ramp/hooks/useAnalytics.test.ts b/app/components/UI/Ramp/hooks/useAnalytics.test.ts
index 126f4baa866..f3a0f101a8e 100644
--- a/app/components/UI/Ramp/hooks/useAnalytics.test.ts
+++ b/app/components/UI/Ramp/hooks/useAnalytics.test.ts
@@ -38,27 +38,4 @@ describe('useAnalytics', () => {
.build(),
);
});
-
- it('calls trackEvent for anonymous params', () => {
- const testEvent = 'RAMP_REGION_SELECTED';
- const testEventParams = {
- country_id: 'test-country-id',
- is_unsupported_offramp: false,
- is_unsupported_onramp: false,
- } as const;
-
- jest.mock('../constants', () => ({
- AnonymousEvents: [testEvent],
- }));
-
- const { result } = renderHookWithProvider(() => useAnalytics());
-
- result.current(testEvent, testEventParams);
-
- expect(MetaMetrics.getInstance().trackEvent).toHaveBeenCalledWith(
- MetricsEventBuilder.createEventBuilder(MetaMetricsEvents[testEvent])
- .addSensitiveProperties(testEventParams)
- .build(),
- );
- });
});
diff --git a/app/components/UI/Ramp/hooks/useAnalytics.ts b/app/components/UI/Ramp/hooks/useAnalytics.ts
index 4e20373ef18..ae52532b5f5 100644
--- a/app/components/UI/Ramp/hooks/useAnalytics.ts
+++ b/app/components/UI/Ramp/hooks/useAnalytics.ts
@@ -1,7 +1,5 @@
import { useCallback } from 'react';
-import { InteractionManager } from 'react-native';
import { AnalyticsEvents } from '../types';
-import { AnonymousEvents } from '../constants';
import { MetaMetrics, MetaMetricsEvents } from '../../../../core/Analytics';
import { MetricsEventBuilder } from '../../../../core/Analytics/MetricsEventBuilder';
@@ -9,19 +7,13 @@ export function trackEvent(
eventType: T,
params: AnalyticsEvents[T],
) {
- const anonymous = AnonymousEvents.includes(eventType);
const metrics = MetaMetrics.getInstance();
- const event = MetricsEventBuilder.createEventBuilder(
- MetaMetricsEvents[eventType],
+ metrics.trackEvent(MetricsEventBuilder.createEventBuilder(
+ MetaMetricsEvents[eventType],
+ )
+ .addProperties({ ...params })
+ .build()
);
-
- InteractionManager.runAfterInteractions(() => {
- if (anonymous) {
- metrics.trackEvent(event.addSensitiveProperties({ ...params }).build());
- } else {
- metrics.trackEvent(event.addProperties({ ...params }).build());
- }
- });
}
function useAnalytics() {
diff --git a/app/components/UI/SDKFeedback/index.tsx b/app/components/UI/SDKFeedback/index.tsx
index eec9e5c2118..63c7c0cd05c 100644
--- a/app/components/UI/SDKFeedback/index.tsx
+++ b/app/components/UI/SDKFeedback/index.tsx
@@ -1,5 +1,4 @@
-import type { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types';
-import type { ThemeTypography } from '@metamask/design-tokens/dist/types/js/typography';
+import type { ThemeColors, ThemeTypography } from '@metamask/design-tokens';
import Icon, {
IconName,
IconSize,
diff --git a/app/components/UI/SDKLoading/index.tsx b/app/components/UI/SDKLoading/index.tsx
index 0d8f92019c1..ccde7ecf154 100644
--- a/app/components/UI/SDKLoading/index.tsx
+++ b/app/components/UI/SDKLoading/index.tsx
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, import/no-commonjs */
-import type { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types';
+import type { ThemeColors } from '@metamask/design-tokens';
import LottieView from 'lottie-react-native';
import React from 'react';
import { StyleSheet, View } from 'react-native';
diff --git a/app/components/UI/SearchTokenAutocomplete/__snapshots__/index.test.tsx.snap b/app/components/UI/SearchTokenAutocomplete/__snapshots__/index.test.tsx.snap
index 1c23219c4e0..e799221d9fe 100644
--- a/app/components/UI/SearchTokenAutocomplete/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/SearchTokenAutocomplete/__snapshots__/index.test.tsx.snap
@@ -327,7 +327,7 @@ exports[`SearchTokenAutocomplete should render correctly 1`] = `
[
{
"alignItems": "center",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"color": "#141618",
diff --git a/app/components/UI/SeedphraseModal/__snapshots__/index.test.tsx.snap b/app/components/UI/SeedphraseModal/__snapshots__/index.test.tsx.snap
index 3ea81e8b70d..106b1b38fb5 100644
--- a/app/components/UI/SeedphraseModal/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/SeedphraseModal/__snapshots__/index.test.tsx.snap
@@ -320,7 +320,7 @@ exports[`SeedphraseModal should render correctly 1`] = `
style={
[
{
- "borderTopColor": "#bbc0c566",
+ "borderTopColor": "#BBC0C566",
"borderTopWidth": 1,
"flexDirection": "row",
"padding": 16,
diff --git a/app/components/UI/SelectOptionSheet/__snapshots__/OptionSheet.test.tsx.snap b/app/components/UI/SelectOptionSheet/__snapshots__/OptionSheet.test.tsx.snap
index a8e5049393f..ea3ed2f39d4 100644
--- a/app/components/UI/SelectOptionSheet/__snapshots__/OptionSheet.test.tsx.snap
+++ b/app/components/UI/SelectOptionSheet/__snapshots__/OptionSheet.test.tsx.snap
@@ -371,14 +371,14 @@ exports[`OptionSheet render matches snapshot 1`] = `
[
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
"borderWidth": 1,
"maxHeight": 1314,
"overflow": "hidden",
"paddingBottom": 0,
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -408,7 +408,7 @@ exports[`OptionSheet render matches snapshot 1`] = `
+ StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ left: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 16,
+ },
+ right: {
+ alignItems: 'flex-end',
+ justifyContent: 'center',
+ },
+ networkAvatar: {
+ height: 32,
+ width: 32,
+ flexShrink: 0,
+ },
+ });
+
+export default styleSheet;
diff --git a/app/components/UI/Stake/components/EarnTokenListItem/EarnTokenListItem.test.tsx b/app/components/UI/Stake/components/EarnTokenListItem/EarnTokenListItem.test.tsx
new file mode 100644
index 00000000000..40e28970ca4
--- /dev/null
+++ b/app/components/UI/Stake/components/EarnTokenListItem/EarnTokenListItem.test.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { EarnTokenListItemProps } from './EarnTokenListItem.types';
+import EarnTokenListItem from '.';
+import { strings } from '../../../../../../locales/i18n';
+import renderWithProvider from '../../../../../util/test/renderWithProvider';
+import { useSelector } from 'react-redux';
+import { selectIsIpfsGatewayEnabled } from '../../../../../selectors/preferencesController';
+import {
+ TextColor,
+ TextVariant,
+} from '../../../../../component-library/components/Texts/Text';
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest.fn(),
+}));
+
+describe('EarnTokenListItem', () => {
+ beforeEach(() => {
+ (useSelector as jest.Mock).mockImplementation((selector) => {
+ if (selector === selectIsIpfsGatewayEnabled) return true;
+ });
+ });
+
+ afterEach(() => {
+ (useSelector as jest.Mock).mockClear();
+ });
+
+ const baseProps: EarnTokenListItemProps = {
+ token: {
+ chainId: '0x1',
+ image:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b175474e89094c44da98b954eedeac495271d0f.png',
+ name: 'Dai Stablecoin',
+ symbol: 'DAI',
+ address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ aggregators: [],
+ decimals: 18,
+ balance: '',
+ balanceFiat: '',
+ logo: undefined,
+ isETH: false,
+ },
+ primaryText: {
+ value: `3.0% ${strings('stake.apr')}`,
+ variant: TextVariant.BodyMDBold,
+ color: TextColor.Success,
+ },
+ onPress: jest.fn(),
+ };
+
+ const secondaryText = {
+ value: '10,100.00 USDC',
+ variant: TextVariant.BodySMBold,
+ color: TextColor.Alternative,
+ };
+
+ it('render matches snapshot', () => {
+ const props: EarnTokenListItemProps = {
+ ...baseProps,
+ secondaryText,
+ };
+
+ const { toJSON } = renderWithProvider();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('renders primary text and secondary text', () => {
+ const props: EarnTokenListItemProps = {
+ ...baseProps,
+ secondaryText,
+ };
+
+ const { getByText } = renderWithProvider();
+
+ expect(getByText('Dai Stablecoin')).toBeDefined();
+ expect(getByText('10,100.00 USDC')).toBeDefined();
+ });
+
+ it('renders only primary text', () => {
+ const { getByText } = renderWithProvider(
+ ,
+ );
+
+ expect(getByText('Dai Stablecoin')).toBeDefined();
+ });
+});
diff --git a/app/components/UI/Stake/components/EarnTokenListItem/EarnTokenListItem.types.ts b/app/components/UI/Stake/components/EarnTokenListItem/EarnTokenListItem.types.ts
new file mode 100644
index 00000000000..7f1152fda81
--- /dev/null
+++ b/app/components/UI/Stake/components/EarnTokenListItem/EarnTokenListItem.types.ts
@@ -0,0 +1,13 @@
+import { TextProps } from '../../../../../component-library/components/Texts/Text/Text.types';
+import { TokenI } from '../../../Tokens/types';
+
+interface Text extends Omit {
+ value: string;
+}
+
+export interface EarnTokenListItemProps {
+ token: TokenI;
+ primaryText: Text;
+ secondaryText?: Text;
+ onPress: (token: TokenI) => void;
+}
diff --git a/app/components/UI/Stake/components/EarnTokenListItem/__snapshots__/EarnTokenListItem.test.tsx.snap b/app/components/UI/Stake/components/EarnTokenListItem/__snapshots__/EarnTokenListItem.test.tsx.snap
new file mode 100644
index 00000000000..c23dc7fe98c
--- /dev/null
+++ b/app/components/UI/Stake/components/EarnTokenListItem/__snapshots__/EarnTokenListItem.test.tsx.snap
@@ -0,0 +1,200 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EarnTokenListItem render matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dai Stablecoin
+
+
+
+
+ 3.0% [missing "en.stake.apr" translation]
+
+
+ 10,100.00 USDC
+
+
+
+`;
diff --git a/app/components/UI/Stake/components/EarnTokenListItem/index.tsx b/app/components/UI/Stake/components/EarnTokenListItem/index.tsx
new file mode 100644
index 00000000000..3f2567ba214
--- /dev/null
+++ b/app/components/UI/Stake/components/EarnTokenListItem/index.tsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import { TouchableOpacity, View } from 'react-native';
+import { useSelector } from 'react-redux';
+import Badge, {
+ BadgeVariant,
+} from '../../../../../component-library/components/Badges/Badge';
+import BadgeWrapper from '../../../../../component-library/components/Badges/BadgeWrapper';
+import Text, {
+ TextColor,
+ TextVariant,
+} from '../../../../../component-library/components/Texts/Text';
+import { selectNetworkName } from '../../../../../selectors/networkInfos';
+import { useStyles } from '../../../../hooks/useStyles';
+import { EarnTokenListItemProps } from './EarnTokenListItem.types';
+import { getNetworkImageSource } from '../../../../../util/networks';
+import styleSheet from './EarnTokenListItem.styles';
+import { AvatarSize } from '../../../../../component-library/components/Avatars/Avatar';
+import AvatarToken from '../../../../../component-library/components/Avatars/Avatar/variants/AvatarToken';
+import NetworkAssetLogo from '../../../NetworkAssetLogo';
+import { TokenI } from '../../../Tokens/types';
+
+interface EarnNetworkAvatarProps {
+ token: TokenI;
+}
+
+const EarnNetworkAvatar = ({ token }: EarnNetworkAvatarProps) => {
+ const { styles } = useStyles(styleSheet, {});
+
+ if (token.isNative) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+const EarnTokenListItem = ({
+ token,
+ primaryText,
+ secondaryText,
+ onPress,
+}: EarnTokenListItemProps) => {
+ const { styles } = useStyles(styleSheet, {});
+
+ const networkName = useSelector(selectNetworkName);
+
+ return (
+ onPress(token)}>
+
+
+ }
+ >
+
+
+ {token.name}
+
+
+
+ {primaryText.value}
+
+ {secondaryText?.value && (
+
+ {secondaryText.value}
+
+ )}
+
+
+ );
+};
+
+export default EarnTokenListItem;
diff --git a/app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap b/app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap
index 1643b96b393..7524889bc42 100644
--- a/app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap
+++ b/app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap
@@ -81,14 +81,14 @@ exports[`GasImpactModal render matches snapshot 1`] = `
[
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
"borderWidth": 1,
"maxHeight": 1334,
"overflow": "hidden",
"paddingBottom": 0,
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -118,7 +118,7 @@ exports[`GasImpactModal render matches snapshot 1`] = `
void;
+}
+
+export const Result: React.FC = memo(({ result, onPress }) => {
+ const theme = useTheme();
+ const styles = stylesheet({theme});
+
+ const name = typeof result.name === 'string' ? result.name : getHost(result.url);
+
+ const dispatch = useDispatch();
+
+ const onPressRemove = useCallback(() => {
+ dispatch(removeBookmark(result));
+ }, [dispatch, result]);
+
+ return (
+
+
+
+
+
+ {name}
+
+
+ {result.url}
+
+
+ {
+ result.type === 'favorites' && (
+
+ )
+ }
+
+
+ );
+});
diff --git a/app/components/UI/UrlAutocomplete/UrlAutocomplete.constants.ts b/app/components/UI/UrlAutocomplete/UrlAutocomplete.constants.ts
new file mode 100644
index 00000000000..46218e10bd4
--- /dev/null
+++ b/app/components/UI/UrlAutocomplete/UrlAutocomplete.constants.ts
@@ -0,0 +1,2 @@
+export const MAX_RECENTS = 5;
+export const ORDERED_CATEGORIES = ['sites', 'recents', 'favorites'];
diff --git a/app/components/UI/UrlAutocomplete/__snapshots__/index.test.js.snap b/app/components/UI/UrlAutocomplete/__snapshots__/index.test.js.snap
deleted file mode 100644
index 298e7109ac8..00000000000
--- a/app/components/UI/UrlAutocomplete/__snapshots__/index.test.js.snap
+++ /dev/null
@@ -1,44 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`UrlAutocomplete should render correctly 1`] = `
-
-
-
-`;
diff --git a/app/components/UI/UrlAutocomplete/index.test.js b/app/components/UI/UrlAutocomplete/index.test.js
deleted file mode 100644
index e2b7a49feee..00000000000
--- a/app/components/UI/UrlAutocomplete/index.test.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-import renderWithProvider from '../../../util/test/renderWithProvider';
-import UrlAutocomplete from './';
-
-describe('UrlAutocomplete', () => {
- it('should render correctly', () => {
- const { toJSON } = renderWithProvider(, {});
- expect(toJSON()).toMatchSnapshot();
- });
-});
diff --git a/app/components/UI/UrlAutocomplete/index.test.tsx b/app/components/UI/UrlAutocomplete/index.test.tsx
new file mode 100644
index 00000000000..f47a6b266d9
--- /dev/null
+++ b/app/components/UI/UrlAutocomplete/index.test.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import UrlAutocomplete, { UrlAutocompleteRef } from './';
+import { deleteFavoriteTestId } from '../../../../wdio/screen-objects/testIDs/BrowserScreen/UrlAutocomplete.testIds';
+import { act, fireEvent, screen } from '@testing-library/react-native';
+import renderWithProvider from '../../../util/test/renderWithProvider';
+import { removeBookmark } from '../../../actions/bookmarks';
+import { noop } from 'lodash';
+
+const defaultState = { browser: { history: [] }, bookmarks: [{url: 'https://www.bookmark.com', name: 'MyBookmark'}] };
+
+describe('UrlAutocomplete', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('should show sites from dapp list', async () => {
+ const ref = React.createRef();
+ renderWithProvider(, {state: defaultState}, false);
+
+ act(() => {
+ ref.current?.search('uni');
+ jest.runAllTimers();
+ });
+
+ expect(await screen.findByText('Uniswap', {includeHiddenElements: true})).toBeDefined();
+ });
+
+ it('should show sites from bookmarks', async () => {
+ const ref = React.createRef();
+ renderWithProvider(, {state: defaultState}, false);
+
+ act(() => {
+ ref.current?.search('MyBook');
+ jest.runAllTimers();
+ });
+
+ expect(await screen.findByText('MyBookmark', {includeHiddenElements: true})).toBeDefined();
+ });
+
+ it('should delete a bookmark when pressing the trash icon', async () => {
+ const ref = React.createRef();
+ const { store } = renderWithProvider(, {state: defaultState}, false);
+ store.dispatch = jest.fn();
+
+ act(() => {
+ ref.current?.search('MyBook');
+ jest.runAllTimers();
+ });
+
+ const deleteFavorite = await screen.findByTestId(deleteFavoriteTestId(defaultState.bookmarks[0].url), {includeHiddenElements: true});
+ fireEvent.press(deleteFavorite);
+ expect(store.dispatch).toHaveBeenCalledWith(removeBookmark({...defaultState.bookmarks[0], type: 'favorites'}));
+ });
+});
diff --git a/app/components/UI/UrlAutocomplete/index.tsx b/app/components/UI/UrlAutocomplete/index.tsx
index e724dc0c158..a04d43b92f3 100644
--- a/app/components/UI/UrlAutocomplete/index.tsx
+++ b/app/components/UI/UrlAutocomplete/index.tsx
@@ -3,20 +3,19 @@ import React, {
useCallback,
useEffect,
useImperativeHandle,
+ useMemo,
useRef,
useState,
} from 'react';
import {
TouchableWithoutFeedback,
View,
- TouchableOpacity,
Text,
+ SectionList,
} from 'react-native';
import dappUrlList from '../../../util/dapp-url-list';
import Fuse from 'fuse.js';
import { useSelector } from 'react-redux';
-import WebsiteIcon from '../WebsiteIcon';
-import { getHost } from '../../../util/browser';
import styleSheet from './styles';
import { useStyles } from '../../../component-library/hooks';
import {
@@ -24,11 +23,16 @@ import {
FuseSearchResult,
UrlAutocompleteRef,
} from './types';
-import { selectBrowserHistory } from '../../../reducers/browser/selectors';
import { debounce } from 'lodash';
+import { strings } from '../../../../locales/i18n';
+import { selectBrowserBookmarksWithType, selectBrowserHistoryWithType } from '../../../selectors/browser';
+import { MAX_RECENTS, ORDERED_CATEGORIES } from './UrlAutocomplete.constants';
+import { Result } from './Result';
export * from './types';
+const dappsWithType = dappUrlList.map(i => ({...i, type: 'sites'}));
+
/**
* Autocomplete list that appears when the browser url bar is focused
*/
@@ -36,9 +40,11 @@ const UrlAutocomplete = forwardRef<
UrlAutocompleteRef,
UrlAutocompleteComponentProps
>(({ onSelect, onDismiss }, ref) => {
- const [results, setResults] = useState([]);
- // TODO: Browser history hasn't been working for a while. Need to either fix or remove.
- const browserHistory = useSelector(selectBrowserHistory);
+ const [resultsByCategory, setResultsByCategory] = useState<{category: string, data: FuseSearchResult[]}[]>([]);
+ const hasResults = resultsByCategory.length > 0;
+
+ const browserHistory = useSelector(selectBrowserHistoryWithType);
+ const bookmarks = useSelector(selectBrowserBookmarksWithType);
const fuseRef = useRef | null>(null);
const resultsRef = useRef(null);
const { styles } = useStyles(styleSheet, {});
@@ -50,29 +56,59 @@ const UrlAutocomplete = forwardRef<
resultsRef.current?.setNativeProps({ style: { display: 'flex' } });
};
- const search = (text: string) => {
+ const updateResults = useCallback((results: FuseSearchResult[]) => {
+ const newResultsByCategory = ORDERED_CATEGORIES.flatMap((category) => {
+ let data = results.filter((result, index, self) =>
+ result.type === category &&
+ index === self.findIndex(r => r.url === result.url && r.type === result.type)
+ );
+ if (data.length === 0) {
+ return [];
+ }
+ if (category === 'recents') {
+ data = data.slice(0, MAX_RECENTS);
+ }
+ return {
+ category,
+ data,
+ };
+ });
+
+ setResultsByCategory(newResultsByCategory);
+ }, []);
+
+ const latestSearchTerm = useRef(null);
+ const search = useCallback((text: string) => {
+ latestSearchTerm.current = text;
+ if (!text) {
+ updateResults([
+ ...browserHistory,
+ ...bookmarks,
+ ]);
+ return;
+ }
const fuseSearchResult = fuseRef.current?.search(text);
if (Array.isArray(fuseSearchResult)) {
- setResults([...fuseSearchResult]);
+ updateResults([...fuseSearchResult]);
} else {
- setResults([]);
+ updateResults([]);
}
- };
+ }, [updateResults, browserHistory, bookmarks]);
/**
* Debounce the search function
*/
- const debouncedSearchRef = useRef(debounce(search, 500));
+ const debouncedSearch = useMemo(() => debounce(search, 100), [search]);
/**
* Hide the results view
*/
const hide = useCallback(() => {
// Cancel the search
- debouncedSearchRef.current.cancel();
+ debouncedSearch.cancel();
resultsRef.current?.setNativeProps({ style: { display: 'none' } });
- setResults([]);
- }, [setResults]);
+ setResultsByCategory([]);
+ }, [debouncedSearch]);
const dismissAutocomplete = () => {
hide();
@@ -81,26 +117,22 @@ const UrlAutocomplete = forwardRef<
};
useImperativeHandle(ref, () => ({
- search: debouncedSearchRef.current,
+ search: debouncedSearch,
hide,
show,
}));
useEffect(() => {
- const allUrls: FuseSearchResult[] = [browserHistory, ...dappUrlList];
- const singleUrlList: string[] = [];
- const singleUrls: FuseSearchResult[] = [];
- for (const el of allUrls) {
- if (!singleUrlList.includes(el.url)) {
- singleUrlList.push(el.url);
- singleUrls.push(el);
- }
- }
+ const allUrls: FuseSearchResult[] = [
+ ...dappsWithType,
+ ...browserHistory,
+ ...bookmarks,
+ ];
// Create the fuse search
- fuseRef.current = new Fuse(singleUrls, {
+ fuseRef.current = new Fuse(allUrls, {
shouldSort: true,
- threshold: 0.45,
+ threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
@@ -110,56 +142,46 @@ const UrlAutocomplete = forwardRef<
{ name: 'url', weight: 0.5 },
],
});
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- const renderResult = useCallback(
- (url: string, name: string, onPress: () => void) => {
- name = typeof name === 'string' ? name : getHost(url);
-
- return (
-
-
-
-
-
- {name}
-
-
- {url}
-
-
-
-
- );
- },
- [styles],
- );
+ if (latestSearchTerm.current !== null) {
+ search(latestSearchTerm.current);
+ }
+ }, [browserHistory, bookmarks, search]);
- const renderResults = useCallback(
- () =>
- results.slice(0, 3).map((result) => {
- const { url, name } = result;
- const onPress = () => {
- hide();
- onSelect(url);
- };
- return renderResult(url, name, onPress);
- }),
- [results, onSelect, hide, renderResult],
- );
+ const renderSectionHeader = useCallback(({section: {category}}) => (
+ {strings(`autocomplete.${category}`)}
+ ), [styles]);
+
+ const renderItem = useCallback(({item}) => (
+ {
+ hide();
+ onSelect(item.url);
+ }}
+ />
+ ), [hide, onSelect]);
+
+ if (!hasResults) {
+ return (
+
+
+
+
+
+ );
+ }
return (
- {renderResults()}
-
-
-
+ `${item.type}-${item.url}`}
+ renderSectionHeader={renderSectionHeader}
+ renderItem={renderItem}
+ keyboardShouldPersistTaps="handled"
+ />
);
});
diff --git a/app/components/UI/UrlAutocomplete/styles.ts b/app/components/UI/UrlAutocomplete/styles.ts
index eaa7bb06dc0..01a1e36e663 100644
--- a/app/components/UI/UrlAutocomplete/styles.ts
+++ b/app/components/UI/UrlAutocomplete/styles.ts
@@ -1,8 +1,7 @@
import { Theme } from '@metamask/design-tokens';
-import { StyleSheet } from 'react-native';
-import { fontStyles } from '../../../styles/common';
+import { StyleSheet, TextStyle } from 'react-native';
-const styleSheet = ({ theme: { colors } }: { theme: Theme }) =>
+const styleSheet = ({ theme: { colors, typography } }: { theme: Theme }) =>
StyleSheet.create({
wrapper: {
...StyleSheet.absoluteFillObject,
@@ -11,6 +10,15 @@ const styleSheet = ({ theme: { colors } }: { theme: Theme }) =>
display: 'none',
paddingTop: 8,
},
+ contentContainer: {
+ paddingVertical: 15,
+ },
+ category: {
+ color: colors.text.default,
+ padding: 10,
+ backgroundColor: colors.background.default,
+ ...typography.lHeadingSM,
+ } as TextStyle,
bookmarkIco: {
width: 26,
height: 26,
@@ -21,15 +29,13 @@ const styleSheet = ({ theme: { colors } }: { theme: Theme }) =>
fontSize: 12,
},
name: {
- fontSize: 14,
color: colors.text.default,
- ...fontStyles.normal,
- },
+ ...typography.lBodyMDMedium,
+ } as TextStyle,
url: {
- fontSize: 12,
color: colors.text.alternative,
- ...fontStyles.normal,
- },
+ ...typography.lBodySM,
+ } as TextStyle,
item: {
paddingVertical: 8,
marginBottom: 8,
@@ -45,6 +51,9 @@ const styleSheet = ({ theme: { colors } }: { theme: Theme }) =>
bg: {
flex: 1,
},
+ deleteFavorite: {
+ marginLeft: 10,
+ },
});
export default styleSheet;
diff --git a/app/components/UI/UrlAutocomplete/types.ts b/app/components/UI/UrlAutocomplete/types.ts
index e86416259ab..daa9e46cede 100644
--- a/app/components/UI/UrlAutocomplete/types.ts
+++ b/app/components/UI/UrlAutocomplete/types.ts
@@ -41,4 +41,5 @@ export type UrlAutocompleteRef = {
export type FuseSearchResult = {
url: string;
name: string;
+ type: string;
};
diff --git a/app/components/UI/WalletAccount/__snapshots__/WalletAccount.test.tsx.snap b/app/components/UI/WalletAccount/__snapshots__/WalletAccount.test.tsx.snap
index b67b7e02b1d..3a26b689fcb 100644
--- a/app/components/UI/WalletAccount/__snapshots__/WalletAccount.test.tsx.snap
+++ b/app/components/UI/WalletAccount/__snapshots__/WalletAccount.test.tsx.snap
@@ -4,7 +4,7 @@ exports[`WalletAccount renders correctly 1`] = `
{
+ const actualNav = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualNav,
+ useNavigation: () => ({
+ navigate: mockedNavigate,
+ }),
+ };
+});
+
+const mockTrackEvent = jest.fn();
+jest.mock('../../../components/hooks/useMetrics', () => ({
+ useMetrics: () => ({
+ trackEvent: mockTrackEvent,
+ createEventBuilder: () => ({
+ build: () => ({}),
+ }),
+ }),
+}));
+
+jest.mock('../../../core/Engine', () => ({
+ context: {
+ KeyringController: {
+ addNewAccount: jest.fn(),
+ },
+ },
+ setSelectedAddress: jest.fn(),
+}));
+
+// Mock Logger
+jest.mock('../../../util/Logger', () => ({
+ error: jest.fn(),
+}));
+
+const mockInitialState = {
+ engine: {
+ backgroundState: {
+ AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
+ KeyringController: MOCK_KEYRING_CONTROLLER,
+ },
+ },
+ multichainSettings: {
+ bitcoinSupportEnabled: true,
+ bitcoinTestnetSupportEnabled: true,
+ solanaSupportEnabled: true,
+ },
+};
+
+const mockProps = {
+ onBack: jest.fn(),
+};
+
+describe('AddAccountActions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders correctly', () => {
+ const wrapper = renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: mockInitialState,
+ },
+ );
+ expect(wrapper.toJSON()).toMatchSnapshot();
+ });
+
+ it('shows all account creation options', () => {
+ renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: mockInitialState,
+ },
+ );
+
+ // Check for standard options
+ expect(
+ screen.getByTestId(AddAccountBottomSheetSelectorsIDs.NEW_ACCOUNT_BUTTON),
+ ).toBeDefined();
+ expect(
+ screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.IMPORT_ACCOUNT_BUTTON,
+ ),
+ ).toBeDefined();
+
+ // Check for multichain options
+ expect(screen.getByText('Add a new Solana Account (Beta)')).toBeDefined();
+ expect(screen.getByText('Add a new Bitcoin Account (Beta)')).toBeDefined();
+ expect(
+ screen.getByText('Add a new Bitcoin Account (Testnet)'),
+ ).toBeDefined();
+ });
+
+ it('creates new ETH account when clicking add new account', async () => {
+ const mockNewAddress = '0x123';
+ (
+ Engine.context.KeyringController.addNewAccount as jest.Mock
+ ).mockResolvedValueOnce(mockNewAddress);
+
+ renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: mockInitialState,
+ },
+ );
+
+ const addButton = screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.NEW_ACCOUNT_BUTTON,
+ );
+ fireEvent.press(addButton);
+
+ await waitFor(() => {
+ expect(Engine.context.KeyringController.addNewAccount).toHaveBeenCalled();
+ expect(Engine.setSelectedAddress).toHaveBeenCalledWith(mockNewAddress);
+ expect(mockProps.onBack).toHaveBeenCalled();
+ });
+ });
+
+ it('handles error when creating new ETH account fails', async () => {
+ const mockError = new Error('Failed to create account');
+ (
+ Engine.context.KeyringController.addNewAccount as jest.Mock
+ ).mockRejectedValueOnce(mockError);
+
+ renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: mockInitialState,
+ },
+ );
+
+ const addButton = screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.NEW_ACCOUNT_BUTTON,
+ );
+ fireEvent.press(addButton);
+
+ await waitFor(() => {
+ expect(Logger.error).toHaveBeenCalledWith(
+ mockError,
+ 'error while trying to add a new account',
+ );
+ expect(mockProps.onBack).toHaveBeenCalled();
+ });
+ });
+
+ it('navigates to import screen when clicking import account', () => {
+ renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: mockInitialState,
+ },
+ );
+
+ const importButton = screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.IMPORT_ACCOUNT_BUTTON,
+ );
+ fireEvent.press(importButton);
+
+ expect(mockedNavigate).toHaveBeenCalledWith('ImportPrivateKeyView');
+ expect(mockProps.onBack).toHaveBeenCalled();
+ });
+
+ it('navigates to hardware wallet connection when clicking connect hardware wallet', () => {
+ renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: mockInitialState,
+ },
+ );
+
+ const hardwareWalletButton = screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.ADD_HARDWARE_WALLET_BUTTON,
+ );
+
+ expect(hardwareWalletButton.findByType(Text).props.children).toBe(
+ 'Add hardware wallet',
+ );
+ fireEvent.press(hardwareWalletButton);
+
+ expect(mockedNavigate).toHaveBeenCalledWith(Routes.HW.CONNECT);
+ expect(mockProps.onBack).toHaveBeenCalled();
+ });
+
+ describe('Multichain account creation', () => {
+ const MOCK_SOL_ADDRESS = 'ATrXkbX2eEPuusRoLyRMW88wcPT2aho2Lk3xErnjjFH';
+ const MOCK_BTC_MAINNET_ADDRESS =
+ 'bc1qkv7xptmd7ejmnnd399z9p643updvula5j4g4nd';
+
+ const solAccount = createMockInternalAccount(
+ MOCK_SOL_ADDRESS,
+ 'Solana Account',
+ KeyringTypes.snap,
+ SolAccountType.DataAccount,
+ );
+
+ const btcMainnetAccount = createMockInternalAccount(
+ MOCK_BTC_MAINNET_ADDRESS,
+ 'Bitcoin Account',
+ KeyringTypes.snap,
+ BtcAccountType.P2wpkh,
+ );
+
+ it('does not disable Solana account creation when account already exists', () => {
+ const stateWithSolAccount = {
+ ...mockInitialState,
+ engine: {
+ ...mockInitialState.engine,
+ backgroundState: {
+ ...mockInitialState.engine.backgroundState,
+ AccountsController: {
+ ...MOCK_ACCOUNTS_CONTROLLER_STATE,
+ internalAccounts: {
+ ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts,
+ accounts: {
+ ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts.accounts,
+ [solAccount.id]: solAccount,
+ },
+ },
+ },
+ },
+ },
+ };
+
+ renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: stateWithSolAccount,
+ },
+ );
+
+ const solButton = screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.ADD_SOLANA_ACCOUNT_BUTTON,
+ );
+ expect(solButton.findByType(Text).props.children).toBe(
+ 'Add a new Solana Account (Beta)',
+ );
+ expect(solButton.props.disabled).toBe(false);
+ });
+
+ it('disables Bitcoin account creation when account already exists', () => {
+ const stateWithBtcAccount = {
+ ...mockInitialState,
+ engine: {
+ ...mockInitialState.engine,
+ backgroundState: {
+ ...mockInitialState.engine.backgroundState,
+ AccountsController: {
+ ...MOCK_ACCOUNTS_CONTROLLER_STATE,
+ internalAccounts: {
+ ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts,
+ accounts: {
+ ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts.accounts,
+ [btcMainnetAccount.id]: btcMainnetAccount,
+ },
+ },
+ },
+ },
+ },
+ };
+
+ renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: stateWithBtcAccount,
+ },
+ );
+
+ const btcButton = screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.ADD_BITCOIN_ACCOUNT_BUTTON,
+ );
+ expect(btcButton.findByType(Text).props.children).toBe(
+ 'Add a new Bitcoin Account (Beta)',
+ );
+ expect(btcButton.props.disabled).toBe(true);
+ });
+
+ it('handles error when creating Bitcoin account fails', async () => {
+ renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: mockInitialState,
+ },
+ );
+
+ const btcButton = screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.ADD_BITCOIN_ACCOUNT_BUTTON,
+ );
+ fireEvent.press(btcButton);
+
+ await waitFor(() => {
+ expect(Logger.error).toHaveBeenCalledWith(
+ expect.any(Error),
+ 'Bitcoin account creation failed',
+ );
+ expect(mockProps.onBack).toHaveBeenCalled();
+ });
+ });
+
+ it('handles error when creating Solana account fails', async () => {
+ renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: mockInitialState,
+ },
+ );
+
+ const solButton = screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.ADD_SOLANA_ACCOUNT_BUTTON,
+ );
+ fireEvent.press(solButton);
+
+ await waitFor(() => {
+ expect(Logger.error).toHaveBeenCalledWith(
+ expect.any(Error),
+ 'Solana account creation failed',
+ );
+ expect(mockProps.onBack).toHaveBeenCalled();
+ });
+ });
+
+ it('disables all buttons while loading', async () => {
+ renderScreen(
+ () => ,
+ {
+ name: 'AddAccountActions',
+ },
+ {
+ state: mockInitialState,
+ },
+ );
+
+ const addButton = screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.NEW_ACCOUNT_BUTTON,
+ );
+ fireEvent.press(addButton);
+
+ // Check that all buttons are disabled while loading
+ expect(
+ screen.getByTestId(AddAccountBottomSheetSelectorsIDs.NEW_ACCOUNT_BUTTON)
+ .props.disabled,
+ ).toBe(true);
+ expect(
+ screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.IMPORT_ACCOUNT_BUTTON,
+ ).props.disabled,
+ ).toBe(true);
+ expect(
+ screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.ADD_SOLANA_ACCOUNT_BUTTON,
+ ).props.disabled,
+ ).toBe(true);
+ expect(
+ screen.getByTestId(
+ AddAccountBottomSheetSelectorsIDs.ADD_BITCOIN_ACCOUNT_BUTTON,
+ ).props.disabled,
+ ).toBe(true);
+ });
+ });
+});
diff --git a/app/components/Views/AddAccountActions/AddAccountActions.tsx b/app/components/Views/AddAccountActions/AddAccountActions.tsx
index 80c2d8021c7..7bff482b2f1 100644
--- a/app/components/Views/AddAccountActions/AddAccountActions.tsx
+++ b/app/components/Views/AddAccountActions/AddAccountActions.tsx
@@ -22,6 +22,7 @@ import { useMetrics } from '../../../components/hooks/useMetrics';
import { CaipChainId } from '@metamask/utils';
import { KeyringClient } from '@metamask/keyring-snap-client';
import { BitcoinWalletSnapSender } from '../../../core/SnapKeyring/BitcoinWalletSnap';
+import { SolanaWalletSnapSender } from '../../../core/SnapKeyring/SolanaWalletSnap';
import { MultichainNetworks } from '../../../core/Multichain/constants';
import { useSelector } from 'react-redux';
import {
@@ -31,6 +32,7 @@ import {
import {
selectIsBitcoinSupportEnabled,
selectIsBitcoinTestnetSupportEnabled,
+ selectIsSolanaSupportEnabled,
} from '../../../selectors/multichain';
///: END:ONLY_INCLUDE_IF
@@ -87,12 +89,15 @@ const AddAccountActions = ({ onBack }: AddAccountActionsProps) => {
selectIsBitcoinTestnetSupportEnabled,
);
+ const isSolanaSupportEnabled = useSelector(selectIsSolanaSupportEnabled);
+
const isBtcMainnetAccountAlreadyCreated = useSelector(
hasCreatedBtcMainnetAccount,
);
const isBtcTestnetAccountAlreadyCreated = useSelector(
hasCreatedBtcTestnetAccount,
);
+
const createBitcoinAccount = async (scope: CaipChainId) => {
try {
setIsLoading(true);
@@ -110,6 +115,24 @@ const AddAccountActions = ({ onBack }: AddAccountActionsProps) => {
setIsLoading(false);
}
};
+
+ const createSolanaAccount = async (scope: CaipChainId) => {
+ try {
+ setIsLoading(true);
+ // Client to create the account using the Solana Snap
+ const client = new KeyringClient(new SolanaWalletSnapSender());
+
+ // This will trigger the Snap account creation flow (+ account renaming)
+ await client.createAccount({
+ scope,
+ });
+ } catch (error) {
+ Logger.error(error as Error, 'Solana account creation failed');
+ } finally {
+ onBack();
+ setIsLoading(false);
+ }
+ };
///: END:ONLY_INCLUDE_IF
return (
@@ -130,6 +153,19 @@ const AddAccountActions = ({ onBack }: AddAccountActionsProps) => {
{
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
}
+ {isSolanaSupportEnabled && (
+ {
+ await createSolanaAccount(MultichainNetworks.SOLANA);
+ }}
+ disabled={isLoading}
+ testID={
+ AddAccountBottomSheetSelectorsIDs.ADD_SOLANA_ACCOUNT_BUTTON
+ }
+ />
+ )}
{isBitcoinSupportEnabled && (
{
await createBitcoinAccount(MultichainNetworks.BITCOIN);
}}
disabled={isLoading || isBtcMainnetAccountAlreadyCreated}
+ testID={
+ AddAccountBottomSheetSelectorsIDs.ADD_BITCOIN_ACCOUNT_BUTTON
+ }
/>
)}
{isBitcoinTestnetSupportEnabled && (
@@ -152,6 +191,9 @@ const AddAccountActions = ({ onBack }: AddAccountActionsProps) => {
await createBitcoinAccount(MultichainNetworks.BITCOIN_TESTNET);
}}
disabled={isLoading || isBtcTestnetAccountAlreadyCreated}
+ testID={
+ AddAccountBottomSheetSelectorsIDs.ADD_BITCOIN_TESTNET_ACCOUNT_BUTTON
+ }
/>
)}
{
@@ -169,6 +211,9 @@ const AddAccountActions = ({ onBack }: AddAccountActionsProps) => {
iconName={IconName.Hardware}
onPress={openConnectHardwareWallet}
disabled={isLoading}
+ testID={
+ AddAccountBottomSheetSelectorsIDs.ADD_HARDWARE_WALLET_BUTTON
+ }
/>
diff --git a/app/components/Views/AddAccountActions/__snapshots__/AddAccountActions.test.tsx.snap b/app/components/Views/AddAccountActions/__snapshots__/AddAccountActions.test.tsx.snap
new file mode 100644
index 00000000000..c6875d28e22
--- /dev/null
+++ b/app/components/Views/AddAccountActions/__snapshots__/AddAccountActions.test.tsx.snap
@@ -0,0 +1,653 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AddAccountActions renders correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+ AddAccountActions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add account
+
+
+
+
+
+
+
+ Add new account
+
+
+
+
+
+ Add a new Solana Account (Beta)
+
+
+
+
+
+ Add a new Bitcoin Account (Beta)
+
+
+
+
+
+ Add a new Bitcoin Account (Testnet)
+
+
+
+
+
+ Import account
+
+
+
+
+
+ Add hardware wallet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/Views/AddAsset/__snapshots__/AddAsset.test.tsx.snap b/app/components/Views/AddAsset/__snapshots__/AddAsset.test.tsx.snap
index 9ba957f6497..030876693aa 100644
--- a/app/components/Views/AddAsset/__snapshots__/AddAsset.test.tsx.snap
+++ b/app/components/Views/AddAsset/__snapshots__/AddAsset.test.tsx.snap
@@ -21,7 +21,7 @@ exports[`AddAsset component renders correctly 1`] = `
>
diff --git a/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.stories.tsx b/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.stories.tsx
index 3bf776efd37..f2684e82e9d 100644
--- a/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.stories.tsx
+++ b/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.stories.tsx
@@ -39,6 +39,7 @@ export const Default = Template.bind(
{
displayBuyButton: true,
displaySwapsButton: true,
+ swapsIsLive: true,
onBuy: () => null,
goToSwaps: () => null,
goToBridge: () => null,
@@ -52,6 +53,7 @@ export const NoBuyButton = Template.bind(
{
displayBuyButton: false,
displaySwapsButton: true,
+ swapsIsLive: true,
onBuy: () => null,
goToSwaps: () => null,
goToBridge: () => null,
@@ -65,6 +67,7 @@ export const NoSwapsButton = Template.bind(
{
displayBuyButton: true,
displaySwapsButton: false,
+ swapsIsLive: false,
onBuy: () => null,
goToSwaps: () => null,
goToBridge: () => null,
@@ -78,6 +81,7 @@ export const NoButtons = Template.bind(
{
displayBuyButton: false,
displaySwapsButton: false,
+ swapsIsLive: false,
onBuy: () => null,
goToSwaps: () => null,
goToBridge: () => null,
diff --git a/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.test.tsx b/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.test.tsx
index 430a9436c14..6cb33d4f688 100644
--- a/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.test.tsx
+++ b/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.test.tsx
@@ -21,6 +21,7 @@ describe('AssetDetailsActions', () => {
const defaultProps = {
displayBuyButton: true,
displaySwapsButton: true,
+ swapsIsLive: true,
onBuy: mockOnBuy,
goToSwaps: mockGoToSwaps,
goToBridge: mockGoToBridge,
diff --git a/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.tsx b/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.tsx
index 38ee31abeac..f387a0c4f59 100644
--- a/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.tsx
+++ b/app/components/Views/AssetDetails/AssetDetailsActions/AssetDetailsActions.tsx
@@ -16,6 +16,7 @@ import { selectCanSignTransactions } from '../../../../selectors/accountsControl
export interface AssetDetailsActionsProps {
displayBuyButton: boolean | undefined;
displaySwapsButton: boolean | undefined;
+ swapsIsLive: boolean | undefined;
onBuy: () => void;
goToSwaps: () => void;
goToBridge: () => void;
@@ -26,6 +27,7 @@ export interface AssetDetailsActionsProps {
export const AssetDetailsActions: React.FC = ({
displayBuyButton,
displaySwapsButton,
+ swapsIsLive,
onBuy,
goToSwaps,
goToBridge,
@@ -61,7 +63,7 @@ export const AssetDetailsActions: React.FC = ({
iconStyle={styles.icon}
containerStyle={styles.containerStyle}
iconSize={AvatarSize.Lg}
- disabled={!canSignTransactions}
+ disabled={!canSignTransactions || !swapsIsLive}
actionID={TokenOverviewSelectorsIDs.SWAP_BUTTON}
/>
diff --git a/app/components/Views/BrowserTab/BrowserTab.tsx b/app/components/Views/BrowserTab/BrowserTab.tsx
index b88ab587ac9..be5271e2b9a 100644
--- a/app/components/Views/BrowserTab/BrowserTab.tsx
+++ b/app/components/Views/BrowserTab/BrowserTab.tsx
@@ -5,7 +5,7 @@ import React, {
useCallback,
useMemo,
} from 'react';
-import { View, Alert, BackHandler, ImageSourcePropType } from 'react-native';
+import { View, Alert, BackHandler, ImageSourcePropType, KeyboardAvoidingView, Platform } from 'react-native';
import { isEqual } from 'lodash';
import { WebView, WebViewMessageEvent } from '@metamask/react-native-webview';
import BrowserBottomBar from '../../UI/BrowserBottomBar';
@@ -1305,111 +1305,116 @@ export const BrowserTab: React.FC = ({
*/
return (
-
-
-
- {renderProgressBar()}
-
- {!!entryScriptWeb3 && firstUrlLoaded && (
- <>
- (
-
+
+
+
+ {renderProgressBar()}
+
+ {!!entryScriptWeb3 && firstUrlLoaded && (
+ <>
+ (
+
+ )}
+ source={{
+ uri: allowedInitialUrl,
+ ...(isExternalLink ? { headers: { Cookie: '' } } : null),
+ }}
+ injectedJavaScriptBeforeContentLoaded={entryScriptWeb3}
+ style={styles.webview}
+ onLoadStart={handleWebviewNavigationChange(OnLoadStart)}
+ onLoadEnd={handleWebviewNavigationChange(OnLoadEnd)}
+ onLoadProgress={handleWebviewNavigationChange(OnLoadProgress)}
+ onNavigationStateChange={handleOnNavigationStateChange}
+ onMessage={onMessage}
+ onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
+ allowsInlineMediaPlayback
+ testID={BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID}
+ applicationNameForUserAgent={'WebView MetaMaskMobile'}
+ onFileDownload={handleOnFileDownload}
+ webviewDebuggingEnabled={isTest}
+ />
+ {ipfsBannerVisible && (
+
)}
- source={{
- uri: allowedInitialUrl,
- ...(isExternalLink ? { headers: { Cookie: '' } } : null),
- }}
- injectedJavaScriptBeforeContentLoaded={entryScriptWeb3}
- style={styles.webview}
- onLoadStart={handleWebviewNavigationChange(OnLoadStart)}
- onLoadEnd={handleWebviewNavigationChange(OnLoadEnd)}
- onLoadProgress={handleWebviewNavigationChange(OnLoadProgress)}
- onNavigationStateChange={handleOnNavigationStateChange}
- onMessage={onMessage}
- onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
- allowsInlineMediaPlayback
- testID={BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID}
- applicationNameForUserAgent={'WebView MetaMaskMobile'}
- onFileDownload={handleOnFileDownload}
- webviewDebuggingEnabled={isTest}
- />
- {ipfsBannerVisible && (
-
- )}
- >
- )}
+ >
+ )}
+
+
-
+ {isTabActive && (
+
+ )}
+ {isTabActive && showOptions && (
+
+ )}
+
+ {renderBottomBar()}
+ {isTabActive && renderOnboardingWizard()}
- {isTabActive && (
-
- )}
- {isTabActive && showOptions && (
-
- )}
-
- {renderBottomBar()}
- {isTabActive && renderOnboardingWizard()}
-
+
);
};
diff --git a/app/components/Views/BrowserTab/components/IpfsBanner/__snapshots__/index.test.tsx.snap b/app/components/Views/BrowserTab/components/IpfsBanner/__snapshots__/index.test.tsx.snap
index a64feef4758..9d16c653e74 100644
--- a/app/components/Views/BrowserTab/components/IpfsBanner/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/BrowserTab/components/IpfsBanner/__snapshots__/index.test.tsx.snap
@@ -16,7 +16,7 @@ exports[`IpfsBanner should render banner correctly 1`] = `
-
-
-`;
diff --git a/app/components/Views/Collectible/index.js b/app/components/Views/Collectible/index.js
deleted file mode 100644
index 0af7da45279..00000000000
--- a/app/components/Views/Collectible/index.js
+++ /dev/null
@@ -1,180 +0,0 @@
-import React, { PureComponent } from 'react';
-import { RefreshControl, ScrollView, View, StyleSheet } from 'react-native';
-import PropTypes from 'prop-types';
-import { getNetworkNavbarOptions } from '../../UI/Navbar';
-import { connect } from 'react-redux';
-import Collectibles from '../../UI/Collectibles';
-import CollectibleContractOverview from '../../UI/CollectibleContractOverview';
-import Engine from '../../../core/Engine';
-import Modal from 'react-native-modal';
-import CollectibleContractInformation from '../../UI/CollectibleContractInformation';
-import { toggleCollectibleContractModal } from '../../../actions/modals';
-import { toLowerCaseEquals } from '../../../util/general';
-import { collectiblesSelector } from '../../../reducers/collectibles';
-import { ThemeContext, mockTheme } from '../../../util/theme';
-
-const createStyles = (colors) =>
- StyleSheet.create({
- wrapper: {
- backgroundColor: colors.background.default,
- flex: 1,
- },
- });
-
-/**
- * View that displays a specific collectible
- * including the overview (name, address, symbol, logo, description, total supply)
- * and also individual collectibles list
- */
-class Collectible extends PureComponent {
- static propTypes = {
- /**
- * Array of assets (in this case Collectibles)
- */
- collectibles: PropTypes.array,
- /**
- /* navigation object required to access the props
- /* passed by the parent component
- */
- navigation: PropTypes.object,
- /**
- * Called to toggle collectible contract information modal
- */
- toggleCollectibleContractModal: PropTypes.func,
- /**
- * Whether collectible contract information is visible
- */
- collectibleContractModalVisible: PropTypes.bool,
- /**
- * Object that represents the current route info like params passed to it
- */
- route: PropTypes.object,
- };
-
- state = {
- refreshing: false,
- collectibles: [],
- };
-
- updateNavBar = () => {
- const { navigation, route } = this.props;
- const colors = this.context.colors || mockTheme.colors;
- getNetworkNavbarOptions(
- route.params?.name ?? '',
- false,
- navigation,
- colors,
- );
- };
-
- componentDidMount = () => {
- this.updateNavBar();
- };
-
- componentDidUpdate = () => {
- this.updateNavBar();
- };
-
- onRefresh = async () => {
- this.setState({ refreshing: true });
- const { NftDetectionController } = Engine.context;
- try {
- await NftDetectionController.detectNfts();
- } finally {
- this.setState({ refreshing: false });
- }
- };
-
- hideCollectibleContractModal = () => {
- this.props.toggleCollectibleContractModal();
- };
-
- render = () => {
- const {
- route: { params },
- navigation,
- collectibleContractModalVisible,
- } = this.props;
- const collectibleContract = params;
- const address = params.address;
- const { collectibles } = this.props;
- const colors = this.context.colors || mockTheme.colors;
- const styles = createStyles(colors);
- const filteredCollectibles = collectibles.filter((collectible) =>
- toLowerCaseEquals(collectible.address, address),
- );
- filteredCollectibles.map((collectible) => {
- if (!collectible.name || collectible.name === '') {
- collectible.name = collectibleContract.name;
- }
- if (!collectible.image && collectibleContract.logo) {
- collectible.image = collectibleContract.logo;
- }
- return collectible;
- });
-
- const ownerOf = filteredCollectibles.length;
-
- return (
-
-
- }
- style={styles.wrapper}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- };
-}
-
-const mapStateToProps = (state) => ({
- collectibles: collectiblesSelector(state),
- collectibleContractModalVisible: state.modals.collectibleContractModalVisible,
-});
-
-const mapDispatchToProps = (dispatch) => ({
- toggleCollectibleContractModal: () =>
- dispatch(toggleCollectibleContractModal()),
-});
-
-Collectible.contextType = ThemeContext;
-
-export default connect(mapStateToProps, mapDispatchToProps)(Collectible);
diff --git a/app/components/Views/Collectible/index.test.tsx b/app/components/Views/Collectible/index.test.tsx
deleted file mode 100644
index 5b8343b6c89..00000000000
--- a/app/components/Views/Collectible/index.test.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import Collectible from './';
-import configureMockStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
-import { backgroundState } from '../../../util/test/initial-root-state';
-
-const mockStore = configureMockStore();
-const initialState = {
- engine: {
- backgroundState,
- },
- modals: {
- collectibleContractModalVisible: false,
- },
-};
-const store = mockStore(initialState);
-
-describe('Collectible', () => {
- it('should render correctly', () => {
- const wrapper = shallow(
-
-
- ,
- );
- expect(wrapper).toMatchSnapshot();
- });
-});
diff --git a/app/components/Views/CollectibleView/index.js b/app/components/Views/CollectibleView/index.js
deleted file mode 100644
index 17ecc850d95..00000000000
--- a/app/components/Views/CollectibleView/index.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import React, { PureComponent } from 'react';
-import { ScrollView, View, StyleSheet, Text, SafeAreaView } from 'react-native';
-import PropTypes from 'prop-types';
-import CollectibleOverview from '../../UI/CollectibleOverview';
-import { getNetworkNavbarOptions } from '../../UI/Navbar';
-import StyledButton from '../../UI/StyledButton';
-import { strings } from '../../../../locales/i18n';
-import { fontStyles } from '../../../styles/common';
-import { connect } from 'react-redux';
-import collectiblesTransferInformation from '../../../util/collectibles-transfer';
-import { newAssetTransaction } from '../../../actions/transaction';
-import { ThemeContext, mockTheme } from '../../../util/theme';
-
-const createStyles = (colors) =>
- StyleSheet.create({
- root: {
- flex: 1,
- backgroundColor: colors.background.default,
- },
- wrapper: {
- flex: 0.9,
- },
- buttons: {
- paddingVertical: 15,
- flex: 0.1,
- height: 4,
- },
- button: {
- marginHorizontal: 16,
- flexDirection: 'row',
- },
- buttonText: {
- marginLeft: 8,
- fontSize: 15,
- color: colors.primary.inverse,
- ...fontStyles.bold,
- },
- });
-
-/**
- * View that displays a specific collectible asset
- */
-class CollectibleView extends PureComponent {
- static propTypes = {
- /**
- /* navigation object required to access the props
- /* passed by the parent component
- */
- navigation: PropTypes.object,
- /**
- * Start transaction with asset
- */
- newAssetTransaction: PropTypes.func,
- /**
- * Object that represents the current route info like params passed to it
- */
- route: PropTypes.object,
- };
-
- updateNavBar = () => {
- const { navigation, route } = this.props;
- const colors = this.context.colors || mockTheme.colors;
- getNetworkNavbarOptions(
- route.params?.contractName ?? '',
- false,
- navigation,
- colors,
- );
- };
-
- componentDidMount = () => {
- this.updateNavBar();
- };
-
- componentDidUpdate = () => {
- this.updateNavBar();
- };
-
- onSend = async () => {
- const {
- route: { params },
- } = this.props;
- this.props.newAssetTransaction(params);
- this.props.navigation.navigate('SendFlowView');
- };
-
- render() {
- const {
- route: { params },
- navigation,
- } = this.props;
- const collectible = params;
- const colors = this.context.colors || mockTheme.colors;
- const styles = createStyles(colors);
-
- const lowerAddress = collectible.address.toLowerCase();
- const tradable =
- lowerAddress in collectiblesTransferInformation
- ? collectiblesTransferInformation[lowerAddress].tradable
- : true;
-
- return (
-
-
-
-
-
-
- {tradable && (
-
-
-
- {strings('asset_overview.send_button').toUpperCase()}
-
-
-
- )}
-
- );
- }
-}
-
-CollectibleView.contextType = ThemeContext;
-
-const mapDispatchToProps = (dispatch) => ({
- newAssetTransaction: (selectedAsset) =>
- dispatch(newAssetTransaction(selectedAsset)),
-});
-
-export default connect(null, mapDispatchToProps)(CollectibleView);
diff --git a/app/components/Views/ConnectQRHardware/index.tsx b/app/components/Views/ConnectQRHardware/index.tsx
index b028495afc3..82387c70726 100644
--- a/app/components/Views/ConnectQRHardware/index.tsx
+++ b/app/components/Views/ConnectQRHardware/index.tsx
@@ -30,7 +30,7 @@ import { useMetrics } from '../../../components/hooks/useMetrics';
import type { MetaMaskKeyring as QRKeyring } from '@keystonehq/metamask-airgapped-keyring';
import { KeyringTypes } from '@metamask/keyring-controller';
import { HardwareDeviceTypes } from '../../../constants/keyringTypes';
-import { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types';
+import { ThemeColors } from '@metamask/design-tokens';
import PAGINATION_OPERATIONS from '../../../constants/pagination';
interface IConnectQRHardwareProps {
diff --git a/app/components/Views/DataCollectionModal/__snapshots__/index.test.tsx.snap b/app/components/Views/DataCollectionModal/__snapshots__/index.test.tsx.snap
index 397ea4c37da..367936b0f44 100644
--- a/app/components/Views/DataCollectionModal/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/DataCollectionModal/__snapshots__/index.test.tsx.snap
@@ -70,14 +70,14 @@ exports[`DataCollectionModal should render expected snapshot 1`] = `
[
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
"borderWidth": 1,
"maxHeight": 1333,
"overflow": "hidden",
"paddingBottom": 3,
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -107,7 +107,7 @@ exports[`DataCollectionModal should render expected snapshot 1`] = `
{
expect(toJSON()).toMatchSnapshot();
});
+ it('renders correctly with token chainId', () => {
+ const { getByText, toJSON } = render(
+ ,
+ );
+
+ // Verifying key elements render
+ expect(getByText('0 ABC')).toBeTruthy();
+ expect(getByText('Token address:')).toBeTruthy();
+
+ // Snapshot test
+ expect(toJSON()).toMatchSnapshot();
+ });
+
it('expands token aggregator list on "show more" press', () => {
const { getByText } = renderComponent();
diff --git a/app/components/Views/DetectedTokens/components/Token.tsx b/app/components/Views/DetectedTokens/components/Token.tsx
index 1c113c6ef3f..d1d8ec4a3ba 100644
--- a/app/components/Views/DetectedTokens/components/Token.tsx
+++ b/app/components/Views/DetectedTokens/components/Token.tsx
@@ -112,11 +112,10 @@ const Token = ({ token, selected, toggleSelected }: Props) => {
const tokenExchangeRates = tokenExchangeRatesAllChains[token.chainId];
const tokenBalancesAllChains = useSelector(selectTokensBalances);
const balanceAllChainsForAccount =
- tokenBalancesAllChains[accountAddress as Hex];
- const tokenBalances =
- balanceAllChainsForAccount[(token.chainId as Hex) ?? currentChainId];
- const conversionRateByChainId = useSelector(selectCurrencyRates);
+ tokenBalancesAllChains?.[accountAddress as Hex] ?? {};
const chainIdToUse = token.chainId ?? currentChainId;
+ const tokenBalances = balanceAllChainsForAccount?.[chainIdToUse];
+ const conversionRateByChainId = useSelector(selectCurrencyRates);
const conversionRate =
conversionRateByChainId[CURRENCY_SYMBOL_BY_CHAIN_ID[token.chainId]]
diff --git a/app/components/Views/DetectedTokens/components/__snapshots__/Token.test.tsx.snap b/app/components/Views/DetectedTokens/components/__snapshots__/Token.test.tsx.snap
index fcf1205fb50..c0c83fb43c0 100644
--- a/app/components/Views/DetectedTokens/components/__snapshots__/Token.test.tsx.snap
+++ b/app/components/Views/DetectedTokens/components/__snapshots__/Token.test.tsx.snap
@@ -67,7 +67,7 @@ exports[`Token Component matches snapshot when token is not selected 1`] = `
"height": 32,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -261,7 +261,7 @@ exports[`Token Component matches snapshot when token is not selected 1`] = `
}
tintColors={
{
- "false": "#bbc0c5",
+ "false": "#848c96",
"true": "#0376c9",
}
}
@@ -338,7 +338,7 @@ exports[`Token Component matches snapshot when token is selected 1`] = `
"height": 32,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -532,7 +532,7 @@ exports[`Token Component matches snapshot when token is selected 1`] = `
}
tintColors={
{
- "false": "#bbc0c5",
+ "false": "#848c96",
"true": "#0376c9",
}
}
@@ -609,7 +609,7 @@ exports[`Token Component renders correctly 1`] = `
"height": 32,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -803,7 +803,278 @@ exports[`Token Component renders correctly 1`] = `
}
tintColors={
{
- "false": "#bbc0c5",
+ "false": "#848c96",
+ "true": "#0376c9",
+ }
+ }
+ value={false}
+ />
+
+
+`;
+
+exports[`Token Component renders correctly with token chainId 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 ABC
+
+
+
+ Token address:
+
+
+
+ 0xToke...ress
+
+
+
+
+
+
+
+
+ Token lists: Aggregator1, Aggregator2
+
+
+
+ + 1 more
+
+
+
+
+
+ ({
updateIncomingTransactions: jest.fn(),
}));
+const mockedNavigate = jest.fn();
+const mockedGoBack = jest.fn();
+
+jest.mock('@react-navigation/native', () => {
+ const actualNav = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualNav,
+ useNavigation: () => ({
+ navigate: mockedNavigate,
+ goBack: mockedGoBack,
+ }),
+ };
+});
+
jest.mock('../../../core/Engine', () => ({
getTotalFiatAccountBalance: jest.fn(),
context: {
NetworkController: {
setActiveNetwork: jest.fn(),
setProviderType: jest.fn(),
+ updateNetwork: jest.fn(),
getNetworkClientById: jest.fn().mockReturnValue({ chainId: '0x1' }),
findNetworkClientIdByChainId: jest
.fn()
@@ -70,6 +90,9 @@ jest.mock('../../../core/Engine', () => ({
},
CurrencyRateController: { updateExchangeRate: jest.fn() },
AccountTrackerController: { refresh: jest.fn() },
+ SelectedNetworkController: {
+ setNetworkClientIdForDomain: jest.fn(),
+ },
},
}));
@@ -259,6 +282,31 @@ describe('Network Selector', () => {
expect(toJSON()).toMatchSnapshot();
});
+ it('renders correctly when network UI redesign is enabled and calls setNetworkClientIdForDomain', async () => {
+ const testMock = {
+ networkName: '',
+ networkImageSource: '',
+ domainNetworkClientId: '',
+ chainId: CHAIN_IDS.MAINNET,
+ rpcUrl: '',
+ domainIsConnectedDapp: true,
+ };
+ jest.spyOn(networks, 'isMultichainV1Enabled').mockReturnValue(true);
+ jest
+ .spyOn(selectedNetworkControllerFcts, 'useNetworkInfo')
+ .mockImplementation(() => testMock);
+ (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true);
+ const { getByText } = renderComponent(initialState);
+ const mainnetCell = getByText('Ethereum Mainnet');
+ fireEvent.press(mainnetCell);
+ await waitFor(() => {
+ expect(
+ mockEngine.context.SelectedNetworkController
+ .setNetworkClientIdForDomain,
+ ).toBeCalled();
+ });
+ });
+
it('shows popular networks when UI redesign is enabled', () => {
(isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true);
const { getByText } = renderComponent(initialState);
diff --git a/app/components/Views/NetworkSelector/NetworkSelector.tsx b/app/components/Views/NetworkSelector/NetworkSelector.tsx
index ce2c19daa8b..b8d5315b47f 100644
--- a/app/components/Views/NetworkSelector/NetworkSelector.tsx
+++ b/app/components/Views/NetworkSelector/NetworkSelector.tsx
@@ -30,13 +30,18 @@ import {
selectIsAllNetworks,
selectNetworkConfigurations,
} from '../../../selectors/networkController';
-import { selectShowTestNetworks } from '../../../selectors/preferencesController';
+import {
+ selectShowTestNetworks,
+ selectTokenNetworkFilter,
+} from '../../../selectors/preferencesController';
import Networks, {
getAllNetworks,
getDecimalChainId,
isTestNet,
getNetworkImageSource,
isMainNet,
+ isPortfolioViewEnabled,
+ isMultichainV1Enabled,
} from '../../../util/networks';
import { LINEA_MAINNET, MAINNET } from '../../../constants/network';
import Button from '../../../component-library/components/Buttons/Button/Button';
@@ -83,7 +88,6 @@ import hideProtocolFromUrl from '../../../util/hideProtocolFromUrl';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import { useNetworkInfo } from '../../../selectors/selectedNetworkController';
import { NetworkConfiguration } from '@metamask/network-controller';
-import Logger from '../../../util/Logger';
import RpcSelectionModal from './RpcSelectionModal/RpcSelectionModal';
import {
TraceName,
@@ -96,6 +100,7 @@ import { store } from '../../../store';
import ReusableModal, { ReusableModalRef } from '../../UI/ReusableModal';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Device from '../../../util/device';
+import Logger from '../../../util/Logger';
interface infuraNetwork {
name: string;
@@ -131,6 +136,7 @@ const NetworkSelector = () => {
const sheetRef = useRef(null);
const showTestNetworks = useSelector(selectShowTestNetworks);
const isAllNetwork = useSelector(selectIsAllNetworks);
+ const tokenNetworkFilter = useSelector(selectTokenNetworkFilter);
const safeAreaInsets = useSafeAreaInsets();
const networkConfigurations = useSelector(selectNetworkConfigurations);
@@ -189,7 +195,6 @@ const NetworkSelector = () => {
chainId === CHAIN_IDS.MAINNET ||
chainId === CHAIN_IDS.LINEA_MAINNET ||
PopularList.some((network) => network.chainId === chainId);
-
const { PreferencesController } = Engine.context;
if (!isAllNetwork && isPopularNetwork) {
PreferencesController.setTokenNetworkFilter({
@@ -200,51 +205,6 @@ const NetworkSelector = () => {
[isAllNetwork],
);
- const onRpcSelect = useCallback(
- async (clientId: string, chainId: `0x${string}`) => {
- const { NetworkController } = Engine.context;
-
- const existingNetwork = networkConfigurations[chainId];
- if (!existingNetwork) {
- Logger.error(
- new Error(`No existing network found for chainId: ${chainId}`),
- );
- return;
- }
-
- const indexOfRpc = existingNetwork.rpcEndpoints.findIndex(
- ({ networkClientId }) => clientId === networkClientId,
- );
-
- if (indexOfRpc === -1) {
- Logger.error(
- new Error(
- `RPC endpoint with clientId: ${clientId} not found for chainId: ${chainId}`,
- ),
- );
- return;
- }
-
- // Proceed to update the network with the correct index
- await NetworkController.updateNetwork(existingNetwork.chainId, {
- ...existingNetwork,
- defaultRpcEndpointIndex: indexOfRpc,
- });
-
- // Set the active network
- await NetworkController.setActiveNetwork(clientId);
-
- // Redirect to wallet page
- navigate(Routes.WALLET.HOME, {
- screen: Routes.WALLET.TAB_STACK_FLOW,
- params: {
- screen: Routes.WALLET_VIEW,
- },
- });
- },
- [networkConfigurations, navigate],
- );
-
const [showMultiRpcSelectModal, setShowMultiRpcSelectModal] = useState<{
isVisible: boolean;
chainId: string;
@@ -279,7 +239,7 @@ const NetworkSelector = () => {
const networkConfigurationId =
rpcEndpoints[defaultRpcEndpointIndex].networkClientId;
- if (domainIsConnectedDapp && process.env.MULTICHAIN_V1) {
+ if (domainIsConnectedDapp && isMultichainV1Enabled()) {
SelectedNetworkController.setNetworkClientIdForDomain(
origin,
networkConfigurationId,
@@ -291,11 +251,10 @@ const NetworkSelector = () => {
} catch (error) {
Logger.error(new Error(`Error in setActiveNetwork: ${error}`));
}
- sheetRef.current?.dismissModal();
}
setTokenNetworkFilter(chainId);
- sheetRef.current?.dismissModal();
+ if (!(domainIsConnectedDapp && isMultichainV1Enabled())) sheetRef.current?.dismissModal();
endTrace({ name: TraceName.SwitchCustomNetwork });
endTrace({ name: TraceName.NetworkSwitch });
trackEvent(
@@ -403,8 +362,7 @@ const NetworkSelector = () => {
AccountTrackerController,
SelectedNetworkController,
} = Engine.context;
-
- if (domainIsConnectedDapp && process.env.MULTICHAIN_V1) {
+ if (domainIsConnectedDapp && isMultichainV1Enabled()) {
SelectedNetworkController.setNetworkClientIdForDomain(origin, type);
} else {
const networkConfiguration =
@@ -888,6 +846,25 @@ const NetworkSelector = () => {
const { NetworkController } = Engine.context;
NetworkController.removeNetwork(chainId);
+ // set tokenNetworkFilter
+ if (isPortfolioViewEnabled()) {
+ const { PreferencesController } = Engine.context;
+ if (!isAllNetwork) {
+ PreferencesController.setTokenNetworkFilter({
+ [chainId]: true,
+ });
+ } else {
+ // Remove the chainId from the tokenNetworkFilter
+ const { [chainId]: _, ...newTokenNetworkFilter } = tokenNetworkFilter;
+ PreferencesController.setTokenNetworkFilter({
+ // TODO fix type of preferences controller level
+ // setTokenNetworkFilter in preferences controller accepts Record while tokenNetworkFilter is Record
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ...(newTokenNetworkFilter as any),
+ });
+ }
+ }
+
setShowConfirmDeleteModal({
isVisible: false,
networkName: '',
@@ -1019,7 +996,6 @@ const NetworkSelector = () => {
{
jest.mock('../../../../core/Engine/Engine', () => ({
context: {
+ NetworkController: {
+ setActiveNetwork: jest.fn(),
+ setProviderType: jest.fn(),
+ updateNetwork: jest.fn(),
+ getNetworkClientById: jest.fn().mockReturnValue({ chainId: '0x1' }),
+ findNetworkClientIdByChainId: jest
+ .fn()
+ .mockReturnValue({ chainId: '0x1' }),
+ getNetworkConfigurationByChainId: jest.fn().mockReturnValue({
+ blockExplorerUrls: [],
+ chainId: '0x1',
+ defaultRpcEndpointIndex: 0,
+ name: 'Mainnet',
+ nativeCurrency: 'ETH',
+ rpcEndpoints: [
+ {
+ networkClientId: 'mainnet',
+ type: 'infura',
+ url: 'https://mainnet.infura.io/v3/{infuraProjectId}',
+ },
+ ],
+ }),
+ },
PreferencesController: {
setTokenNetworkFilter: jest.fn(),
},
@@ -151,7 +194,6 @@ describe('RpcSelectionModal', () => {
networkName: 'Mainnet',
},
closeRpcModal: jest.fn(),
- onRpcSelect: jest.fn(),
rpcMenuSheetRef: mockRpcMenuSheetRef,
networkConfigurations: MOCK_STORE_STATE.engine.backgroundState
.NetworkController.networkConfigurations as unknown as Record<
@@ -229,11 +271,7 @@ describe('RpcSelectionModal', () => {
const rpcUrlElement = getByText('mainnet.infura.io/v3');
fireEvent.press(rpcUrlElement);
-
- expect(defaultProps.onRpcSelect).toHaveBeenCalledWith(
- 'mainnet',
- CHAIN_IDS.MAINNET,
- );
+ expect(NetworkController.updateNetwork).toHaveBeenCalled();
expect(defaultProps.closeRpcModal).toHaveBeenCalled();
});
@@ -261,4 +299,45 @@ describe('RpcSelectionModal', () => {
1,
);
});
+
+ it('should not call preferences controller setTokenNetworkFilter when a popular networks filter is selected', () => {
+ (useSelector as jest.Mock).mockImplementation((selector) => {
+ if (selector === selectIsAllNetworks) {
+ return true; // to show all networks
+ }
+ return null;
+ });
+ const { getByText } = renderWithProvider(
+ ,
+ );
+ const rpcUrlElement = getByText('mainnet.infura.io/v3');
+ fireEvent.press(rpcUrlElement);
+ expect(PreferencesController.setTokenNetworkFilter).toHaveBeenCalledTimes(
+ 0,
+ );
+ });
+
+ it('should not call preferences controller setTokenNetworkFilter when the network is not part of PopularList', () => {
+ (useSelector as jest.Mock).mockImplementation((selector) => {
+ if (selector === selectIsAllNetworks) {
+ return false; // to show current network
+ }
+ return null;
+ });
+ const { getByText } = renderWithProvider(
+ ,
+ );
+ const rpcUrlElement = getByText('test.infura.io/v3');
+ fireEvent.press(rpcUrlElement);
+ expect(PreferencesController.setTokenNetworkFilter).toHaveBeenCalledTimes(
+ 0,
+ );
+ });
});
diff --git a/app/components/Views/NetworkSelector/RpcSelectionModal/RpcSelectionModal.tsx b/app/components/Views/NetworkSelector/RpcSelectionModal/RpcSelectionModal.tsx
index ff5f31998af..fd124a62760 100644
--- a/app/components/Views/NetworkSelector/RpcSelectionModal/RpcSelectionModal.tsx
+++ b/app/components/Views/NetworkSelector/RpcSelectionModal/RpcSelectionModal.tsx
@@ -25,6 +25,9 @@ import { useSelector } from 'react-redux';
import { selectIsAllNetworks } from '../../../../selectors/networkController';
import { PopularList } from '../../../../util/networks/customNetworks';
import Engine from '../../../../core/Engine/Engine';
+import Logger from '../../../../util/Logger';
+import { useNavigation } from '@react-navigation/native';
+import Routes from '../../../../constants/navigation/Routes';
interface RpcSelectionModalProps {
showMultiRpcSelectModal: {
@@ -33,7 +36,6 @@ interface RpcSelectionModalProps {
networkName: string;
};
closeRpcModal: () => void;
- onRpcSelect: (networkClientId: string, chainId: `0x${string}`) => void;
rpcMenuSheetRef: React.RefObject;
networkConfigurations: Record;
styles: StyleSheet.NamedStyles<{
@@ -49,13 +51,51 @@ interface RpcSelectionModalProps {
const RpcSelectionModal: FC = ({
showMultiRpcSelectModal,
closeRpcModal,
- onRpcSelect,
rpcMenuSheetRef,
networkConfigurations,
styles,
}) => {
const isAllNetwork = useSelector(selectIsAllNetworks);
+ const { navigate } = useNavigation();
+
+ const onRpcSelect = useCallback(
+ async (clientId: string, chainId: `0x${string}`) => {
+ const { NetworkController } = Engine.context;
+ const existingNetwork = networkConfigurations[chainId];
+
+ const indexOfRpc = existingNetwork.rpcEndpoints.findIndex(
+ ({ networkClientId }) => clientId === networkClientId,
+ );
+
+ if (indexOfRpc === -1) {
+ Logger.error(
+ new Error(
+ `RPC endpoint with clientId: ${clientId} not found for chainId: ${chainId}`,
+ ),
+ );
+ return;
+ }
+
+ // Proceed to update the network with the correct index
+ await NetworkController.updateNetwork(existingNetwork.chainId, {
+ ...existingNetwork,
+ defaultRpcEndpointIndex: indexOfRpc,
+ });
+
+ // Set the active network
+ NetworkController.setActiveNetwork(clientId);
+ // Redirect to wallet page
+ navigate(Routes.WALLET.HOME, {
+ screen: Routes.WALLET.TAB_STACK_FLOW,
+ params: {
+ screen: Routes.WALLET_VIEW,
+ },
+ });
+ },
+ [networkConfigurations, navigate],
+ );
+
const setTokenNetworkFilter = useCallback(
(chainId: string) => {
const isPopularNetwork =
diff --git a/app/components/Views/NetworkSelector/RpcSelectionModal/__snapshots__/RpcSelectionModal.test.tsx.snap b/app/components/Views/NetworkSelector/RpcSelectionModal/__snapshots__/RpcSelectionModal.test.tsx.snap
index 03c7a775a93..cd6340df225 100644
--- a/app/components/Views/NetworkSelector/RpcSelectionModal/__snapshots__/RpcSelectionModal.test.tsx.snap
+++ b/app/components/Views/NetworkSelector/RpcSelectionModal/__snapshots__/RpcSelectionModal.test.tsx.snap
@@ -70,14 +70,14 @@ exports[`RpcSelectionModal should render correctly when visible 1`] = `
[
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
"borderWidth": 1,
"maxHeight": 1333,
"overflow": "hidden",
"paddingBottom": 3,
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -107,7 +107,7 @@ exports[`RpcSelectionModal should render correctly when visible 1`] = `
@@ -1603,7 +1603,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled
style={
{
"alignSelf": "center",
- "backgroundColor": "#bbc0c5",
+ "backgroundColor": "#848c96",
"borderRadius": 4,
"height": 5,
"marginTop": 8,
@@ -1642,7 +1642,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled
style={
{
"alignItems": "center",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 5,
"borderWidth": 1,
"color": "#141618",
@@ -1680,7 +1680,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled
placeholderTextColor="#141618"
style={
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"color": "#141618",
"flex": 1,
"fontFamily": "EuclidCircularB-Regular",
@@ -1741,7 +1741,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled
style={
{
"alignItems": "center",
- "backgroundColor": "#0376c91a",
+ "backgroundColor": "#0376C91A",
"flexDirection": "row",
}
}
@@ -1917,7 +1917,7 @@ exports[`Network Selector renders correctly when network UI redesign is enabled
accessible={true}
style={
{
- "backgroundColor": "#0376c91a",
+ "backgroundColor": "#0376C91A",
"bottom": 0,
"flexDirection": "row",
"left": 0,
@@ -3927,14 +3927,14 @@ exports[`Network Selector renders correctly when network UI redesign is enabled
"width": 51,
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
]
}
testID="test-network-switch-id"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/NftDetails/NftDetailsBox.tsx b/app/components/Views/NftDetails/NftDetailsBox.tsx
index 2aa3f99be9c..572494423fe 100644
--- a/app/components/Views/NftDetails/NftDetailsBox.tsx
+++ b/app/components/Views/NftDetails/NftDetailsBox.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet, View, TouchableOpacity } from 'react-native';
import { useTheme } from '../../../util/theme';
import Text from '../../../component-library/components/Texts/Text';
-import { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types';
+import { ThemeColors } from '@metamask/design-tokens';
import { NftDetailsBoxProps } from './NftDetails.types';
const createStyles = (colors: ThemeColors) =>
diff --git a/app/components/Views/NftDetails/__snapshots__/NftDetails.test.ts.snap b/app/components/Views/NftDetails/__snapshots__/NftDetails.test.ts.snap
index 245d0b1d685..920b73ad221 100644
--- a/app/components/Views/NftDetails/__snapshots__/NftDetails.test.ts.snap
+++ b/app/components/Views/NftDetails/__snapshots__/NftDetails.test.ts.snap
@@ -442,7 +442,7 @@ exports[`NftDetails should render correctly 1`] = `
"height": 16,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -574,7 +574,7 @@ exports[`NftDetails should render correctly 1`] = `
style={
[
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"flexGrow": 1,
@@ -642,7 +642,7 @@ exports[`NftDetails should render correctly 1`] = `
style={
[
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"flexGrow": 1,
@@ -743,7 +743,7 @@ exports[`NftDetails should render correctly 1`] = `
style={
[
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"flexGrow": 1,
@@ -811,7 +811,7 @@ exports[`NftDetails should render correctly 1`] = `
style={
[
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"flexGrow": 1,
@@ -1277,7 +1277,7 @@ exports[`NftDetails should render correctly 1`] = `
style={
[
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"flexGrow": 1,
diff --git a/app/components/Views/Notifications/Details/Badge/__snapshots__/index.test.tsx.snap b/app/components/Views/Notifications/Details/Badge/__snapshots__/index.test.tsx.snap
index efdc00300ff..7f55e9352fd 100644
--- a/app/components/Views/Notifications/Details/Badge/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Notifications/Details/Badge/__snapshots__/index.test.tsx.snap
@@ -121,7 +121,7 @@ exports[`NotificationBadge should renders correctly 1`] = `
"height": 32,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
diff --git a/app/components/Views/Notifications/Details/__snapshots__/index.test.tsx.snap b/app/components/Views/Notifications/Details/__snapshots__/index.test.tsx.snap
index 1d2956543df..001927010ef 100644
--- a/app/components/Views/Notifications/Details/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Notifications/Details/__snapshots__/index.test.tsx.snap
@@ -537,7 +537,7 @@ exports[`NotificationsDetails renders correctly 1`] = `
{
"alignItems": "center",
"alignSelf": "center",
- "backgroundColor": "#1c82341a",
+ "backgroundColor": "#1C82341A",
"borderRadius": 16,
"height": 32,
"justifyContent": "center",
@@ -781,7 +781,7 @@ exports[`NotificationsDetails renders correctly 1`] = `
"height": 32,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
diff --git a/app/components/Views/OnboardingCarousel/index.tsx b/app/components/Views/OnboardingCarousel/index.tsx
index 3a58f067b84..45b8794b1ac 100644
--- a/app/components/Views/OnboardingCarousel/index.tsx
+++ b/app/components/Views/OnboardingCarousel/index.tsx
@@ -8,7 +8,7 @@ import {
Dimensions,
Platform,
} from 'react-native';
-import type { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types';
+import type { ThemeColors } from '@metamask/design-tokens';
import { NavigationProp, ParamListBase } from '@react-navigation/native';
import { MetaMetricsEvents } from '../../../core/Analytics';
import { ITrackingEvent } from '../../../core/Analytics/MetaMetrics.types';
diff --git a/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap
index 8ef7ec8b43c..06c69757b35 100644
--- a/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/OnboardingSuccess/OnboardingAssetsSettings/__snapshots__/index.test.tsx.snap
@@ -74,7 +74,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -82,7 +82,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
}
testID="token-detection-toggle"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -159,7 +159,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -167,7 +167,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
}
testID="nft-display-media-mode-section"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
@@ -244,7 +244,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -252,7 +252,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
}
testID="nft-opensea-autodetect-mode-section"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
@@ -329,7 +329,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -337,7 +337,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
}
testID="IPFS_GATEWAY_SECTION"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -384,7 +384,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
@@ -577,7 +577,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
style={
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 4,
"borderWidth": 0,
"padding": 16,
@@ -671,7 +671,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -679,7 +679,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
}
testID="incoming-linea-mainnet-toggle"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -743,7 +743,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -751,7 +751,7 @@ exports[`OnboardingAssetSettings should render correctly 1`] = `
}
testID="security-settings-multi-account-balances-switch"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
diff --git a/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap
index fe1b8b6e1a7..2f8378aeb94 100644
--- a/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/OnboardingSuccess/OnboardingGeneralSettings/__snapshots__/index.test.tsx.snap
@@ -56,13 +56,13 @@ exports[`OnboardingGeneralSettings should render correctly 1`] = `
"width": 51,
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/__snapshots__/index.test.tsx.snap b/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/__snapshots__/index.test.tsx.snap
index ab1dc767165..2179aa0c73b 100644
--- a/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/OnboardingSuccess/OnboardingSecuritySettings/__snapshots__/index.test.tsx.snap
@@ -67,7 +67,7 @@ exports[`OnboardingSecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -75,7 +75,7 @@ exports[`OnboardingSecuritySettings should render correctly 1`] = `
}
testID="display-use-safe-list-validation"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/OriginSpamModal/__snapshots__/OriginSpamModal.test.tsx.snap b/app/components/Views/OriginSpamModal/__snapshots__/OriginSpamModal.test.tsx.snap
index 160f84abdce..a6de76f711c 100644
--- a/app/components/Views/OriginSpamModal/__snapshots__/OriginSpamModal.test.tsx.snap
+++ b/app/components/Views/OriginSpamModal/__snapshots__/OriginSpamModal.test.tsx.snap
@@ -192,7 +192,7 @@ exports[`OriginSpamModal renders spam modal content by default 1`] = `
"alignItems": "center",
"alignSelf": "center",
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 99,
"borderWidth": 1,
"flexDirection": "row",
diff --git a/app/components/Views/PickComponent/__snapshots__/index.test.tsx.snap b/app/components/Views/PickComponent/__snapshots__/index.test.tsx.snap
index cebc2a1a747..c506b0b2618 100644
--- a/app/components/Views/PickComponent/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/PickComponent/__snapshots__/index.test.tsx.snap
@@ -34,7 +34,7 @@ exports[`PickComponent should render correctly 1`] = `
{
"alignItems": "center",
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 99,
"borderWidth": 2,
"height": 20,
diff --git a/app/components/Views/RestoreWallet/styles.ts b/app/components/Views/RestoreWallet/styles.ts
index 16070c45870..d675612af60 100644
--- a/app/components/Views/RestoreWallet/styles.ts
+++ b/app/components/Views/RestoreWallet/styles.ts
@@ -1,5 +1,5 @@
/* eslint-disable import/prefer-default-export */
-import type { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types';
+import type { ThemeColors } from '@metamask/design-tokens';
import { StyleSheet } from 'react-native';
export const createStyles = (colors: ThemeColors) =>
diff --git a/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap b/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap
index 4a8e888f4a9..9ed02df8787 100644
--- a/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.tsx.snap
@@ -157,7 +157,7 @@ exports[`RevealPrivateCredential renders reveal SRP correctly when the credentia
@@ -335,14 +335,14 @@ exports[`AdvancedSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -421,14 +421,14 @@ exports[`AdvancedSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
@@ -507,7 +507,7 @@ exports[`AdvancedSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -515,7 +515,7 @@ exports[`AdvancedSettings should render correctly 1`] = `
}
testID="token-detection-toggle"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -594,7 +594,7 @@ exports[`AdvancedSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -602,7 +602,7 @@ exports[`AdvancedSettings should render correctly 1`] = `
}
testID="show-fiat-on-testnets"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/Settings/AppInformation/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/AppInformation/__snapshots__/index.test.tsx.snap
index d487516769a..fb22f845532 100644
--- a/app/components/Views/Settings/AppInformation/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/AppInformation/__snapshots__/index.test.tsx.snap
@@ -494,7 +494,7 @@ exports[`AppInformation should render correctly 1`] = `
diff --git a/app/components/Views/Settings/AutoDetectTokensSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/AutoDetectTokensSettings/__snapshots__/index.test.tsx.snap
index 180daab2501..a324351f863 100644
--- a/app/components/Views/Settings/AutoDetectTokensSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/AutoDetectTokensSettings/__snapshots__/index.test.tsx.snap
@@ -58,7 +58,7 @@ exports[`AssetSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -66,7 +66,7 @@ exports[`AssetSettings should render correctly 1`] = `
}
testID="token-detection-toggle"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
diff --git a/app/components/Views/Settings/BatchAccountBalanceSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/BatchAccountBalanceSettings/__snapshots__/index.test.tsx.snap
index b55d2c4c1e5..5d1d1eb96d0 100644
--- a/app/components/Views/Settings/BatchAccountBalanceSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/BatchAccountBalanceSettings/__snapshots__/index.test.tsx.snap
@@ -57,7 +57,7 @@ exports[`BatchAccountBalanceSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -65,7 +65,7 @@ exports[`BatchAccountBalanceSettings should render correctly 1`] = `
}
testID="security-settings-multi-account-balances-switch"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/AmbiguousAddressSheet.styles.ts b/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/AmbiguousAddressSheet.styles.ts
index f03505a6ba7..1b596f291bc 100644
--- a/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/AmbiguousAddressSheet.styles.ts
+++ b/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/AmbiguousAddressSheet.styles.ts
@@ -1,7 +1,6 @@
// Third party dependencies.
import { StyleSheet, TextStyle } from 'react-native';
-import { typography } from '@metamask/design-tokens';
-import type { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types';
+import { typography, ThemeColors } from '@metamask/design-tokens';
/**
* Style sheet function for AmbiguousAddressSheet component.
*
diff --git a/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/__snapshots__/AmbiguousAddressSheet.test.tsx.snap b/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/__snapshots__/AmbiguousAddressSheet.test.tsx.snap
index 4d390b981a9..880a7ab43d5 100644
--- a/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/__snapshots__/AmbiguousAddressSheet.test.tsx.snap
+++ b/app/components/Views/Settings/Contacts/AmbiguousAddressSheet/__snapshots__/AmbiguousAddressSheet.test.tsx.snap
@@ -371,14 +371,14 @@ exports[`AmbiguousAddressSheet should render correctly 1`] = `
[
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
"borderWidth": 1,
"maxHeight": 1314,
"overflow": "hidden",
"paddingBottom": 0,
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -408,7 +408,7 @@ exports[`AmbiguousAddressSheet should render correctly 1`] = `
diff --git a/app/components/Views/Settings/ExperimentalSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/ExperimentalSettings/__snapshots__/index.test.tsx.snap
index 9f338464fc0..71203e9d1d6 100644
--- a/app/components/Views/Settings/ExperimentalSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/ExperimentalSettings/__snapshots__/index.test.tsx.snap
@@ -148,13 +148,13 @@ exports[`ExperimentalSettings should render correctly 1`] = `
"alignSelf": "flex-end",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
]
}
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/Settings/IPFSGatewaySettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/IPFSGatewaySettings/__snapshots__/index.test.tsx.snap
index 33852107326..d6793d0765f 100644
--- a/app/components/Views/Settings/IPFSGatewaySettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/IPFSGatewaySettings/__snapshots__/index.test.tsx.snap
@@ -56,7 +56,7 @@ exports[`IPFSGatewaySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -64,7 +64,7 @@ exports[`IPFSGatewaySettings should render correctly 1`] = `
}
testID="IPFS_GATEWAY_SECTION"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -111,7 +111,7 @@ exports[`IPFSGatewaySettings should render correctly 1`] = `
@@ -179,7 +179,7 @@ exports[`IncomingTransactionsSettings should render correctly 1`] = `
style={
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 4,
"borderWidth": 0,
"padding": 16,
@@ -273,7 +273,7 @@ exports[`IncomingTransactionsSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -281,7 +281,7 @@ exports[`IncomingTransactionsSettings should render correctly 1`] = `
}
testID="incoming-linea-mainnet-toggle"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
diff --git a/app/components/Views/Settings/NetworkDetailsCheckSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/NetworkDetailsCheckSettings/__snapshots__/index.test.tsx.snap
index a1aa508fe9e..a953e19c716 100644
--- a/app/components/Views/Settings/NetworkDetailsCheckSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/NetworkDetailsCheckSettings/__snapshots__/index.test.tsx.snap
@@ -57,7 +57,7 @@ exports[`NetworkDetailsCheckSettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -65,7 +65,7 @@ exports[`NetworkDetailsCheckSettings should render correctly 1`] = `
}
testID="display-use-safe-list-validation"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
index 2d23c4a48bc..b4c95f0c154 100644
--- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
+++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
@@ -21,6 +21,7 @@ import Networks, {
isPrivateConnection,
getAllNetworks,
getIsNetworkOnboarded,
+ isPortfolioViewEnabled,
} from '../../../../../util/networks';
import Engine from '../../../../../core/Engine';
import { isWebUri } from 'valid-url';
@@ -51,6 +52,7 @@ import Button, {
ButtonWidthTypes,
} from '../../../../../component-library/components/Buttons/Button';
import {
+ selectIsAllNetworks,
selectNetworkConfigurations,
selectProviderConfig,
} from '../../../../../selectors/networkController';
@@ -66,7 +68,10 @@ import { updateIncomingTransactions } from '../../../../../util/transaction-cont
import { withMetricsAwareness } from '../../../../../components/hooks/useMetrics';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import Routes from '../../../../../constants/navigation/Routes';
-import { selectUseSafeChainsListValidation } from '../../../../../../app/selectors/preferencesController';
+import {
+ selectTokenNetworkFilter,
+ selectUseSafeChainsListValidation,
+} from '../../../../../../app/selectors/preferencesController';
import withIsOriginalNativeToken from './withIsOriginalNativeToken';
import { compose } from 'redux';
import Icon, {
@@ -436,6 +441,16 @@ export class NetworkSettings extends PureComponent {
* Matched object from third provider
*/
matchedChainNetwork: PropTypes.object,
+
+ /**
+ * Checks if all networks are selected
+ */
+ isAllNetworks: PropTypes.bool,
+
+ /**
+ * Token network filter
+ */
+ tokenNetworkFilter: PropTypes.object,
};
state = {
@@ -888,7 +903,13 @@ export class NetworkSettings extends PureComponent {
} = this.state;
const ticker = this.state.ticker && this.state.ticker.toUpperCase();
- const { navigation, networkOnboardedState, route } = this.props;
+ const {
+ navigation,
+ networkOnboardedState,
+ route,
+ isAllNetworks,
+ tokenNetworkFilter,
+ } = this.props;
const isCustomMainnet = route.params?.isCustomMainnet;
const shouldNetworkSwitchPopToWallet =
@@ -934,6 +955,21 @@ export class NetworkSettings extends PureComponent {
return;
}
+ // Set tokenNetworkFilter
+ if (isPortfolioViewEnabled()) {
+ const { PreferencesController } = Engine.context;
+ if (!isAllNetworks) {
+ PreferencesController.setTokenNetworkFilter({
+ [chainId]: true,
+ });
+ } else {
+ PreferencesController.setTokenNetworkFilter({
+ ...tokenNetworkFilter,
+ [chainId]: true,
+ });
+ }
+ }
+
await this.handleNetworkUpdate({
rpcUrl,
chainId,
@@ -2566,6 +2602,8 @@ const mapStateToProps = (state) => ({
networkConfigurations: selectNetworkConfigurations(state),
networkOnboardedState: state.networkOnboarded.networkOnboardedState,
useSafeChainsListValidation: selectUseSafeChainsListValidation(state),
+ isAllNetworks: selectIsAllNetworks(state),
+ tokenNetworkFilter: selectTokenNetworkFilter(state),
});
export default compose(
diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx
index de3c01ccca5..2c0b8912034 100644
--- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx
+++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx
@@ -12,6 +12,9 @@ import { mockNetworkState } from '../../../../../util/test/network';
import * as jsonRequest from '../../../../../util/jsonRpcRequest';
import Logger from '../../../../../util/Logger';
import Engine from '../../../../../core/Engine';
+// eslint-disable-next-line import/no-namespace
+import * as networks from '../../../../../util/networks';
+const { PreferencesController } = Engine.context;
// Mock the entire module
jest.mock('../../../../../util/networks/isNetworkUiRedesignEnabled', () => ({
@@ -58,6 +61,9 @@ jest.mock('../../../../../core/Engine', () => ({
CurrencyRateController: {
updateExchangeRate: jest.fn(),
},
+ PreferencesController: {
+ setTokenNetworkFilter: jest.fn(),
+ },
},
}));
@@ -1772,6 +1778,44 @@ describe('NetworkSettings', () => {
}),
);
});
+
+ it('should not call setTokenNetworkFilter when portfolio view is disabled', async () => {
+ jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(false);
+ const tokenNetworkFilterSpy = jest.spyOn(
+ PreferencesController,
+ 'setTokenNetworkFilter',
+ );
+
+ wrapper.setState({
+ rpcUrl: 'http://localhost:8545',
+ chainId: '0x1',
+ ticker: 'ETH',
+ nickname: 'Localhost',
+ enableAction: true,
+ });
+
+ await wrapper.instance().addRpcUrl();
+ expect(tokenNetworkFilterSpy).toHaveBeenCalledTimes(0);
+ });
+
+ it('should call setTokenNetworkFilter when portfolio view is enabled', async () => {
+ jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(true);
+ const tokenNetworkFilterSpy = jest.spyOn(
+ PreferencesController,
+ 'setTokenNetworkFilter',
+ );
+
+ wrapper.setState({
+ rpcUrl: 'http://localhost:8545',
+ chainId: '0x1',
+ ticker: 'ETH',
+ nickname: 'Localhost',
+ enableAction: true,
+ });
+
+ await wrapper.instance().addRpcUrl();
+ expect(tokenNetworkFilterSpy).toHaveBeenCalledTimes(1);
+ });
});
describe('checkIfNetworkExists', () => {
diff --git a/app/components/Views/Settings/NetworksSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/NetworksSettings/__snapshots__/index.test.tsx.snap
index cc90b47c6ef..f4277560fce 100644
--- a/app/components/Views/Settings/NetworksSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/NetworksSettings/__snapshots__/index.test.tsx.snap
@@ -382,7 +382,7 @@ exports[`NetworksSettings should render correctly 1`] = `
style={
{
"alignItems": "center",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 5,
"borderWidth": 1,
"color": "#141618",
@@ -420,7 +420,7 @@ exports[`NetworksSettings should render correctly 1`] = `
placeholderTextColor="#141618"
style={
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"color": "#141618",
"flex": 1,
"fontFamily": "EuclidCircularB-Regular",
diff --git a/app/components/Views/Settings/NotificationsSettings/CustomNotificationsRow/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/NotificationsSettings/CustomNotificationsRow/__snapshots__/index.test.tsx.snap
index a397ed5b8c6..2de1081e468 100644
--- a/app/components/Views/Settings/NotificationsSettings/CustomNotificationsRow/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/NotificationsSettings/CustomNotificationsRow/__snapshots__/index.test.tsx.snap
@@ -81,14 +81,14 @@ exports[`CustomNotificationsRow should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap b/app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap
index 621e0158390..350243af16a 100644
--- a/app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap
+++ b/app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap
@@ -268,14 +268,14 @@ exports[`AccountsList matches snapshot 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
@@ -501,14 +501,14 @@ exports[`AccountsList matches snapshot 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/Settings/NotificationsSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/NotificationsSettings/__snapshots__/index.test.tsx.snap
index 1859f635317..1931ade4828 100644
--- a/app/components/Views/Settings/NotificationsSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/NotificationsSettings/__snapshots__/index.test.tsx.snap
@@ -55,14 +55,14 @@ exports[`NotificationsSettings render matches snapshot 1`] = `
"alignSelf": "flex-end",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx b/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx
index 49da558a078..3952a885a3b 100644
--- a/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx
+++ b/app/components/Views/Settings/PermissionsSettings/PermissionsManager.tsx
@@ -7,8 +7,7 @@ import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context';
import { strings } from '../../../../../locales/i18n';
import { useTheme } from '../../../../util/theme';
-import type { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types';
-import type { ThemeTypography } from '@metamask/design-tokens/dist/types/js/typography';
+import type { ThemeColors, ThemeTypography } from '@metamask/design-tokens';
import { SDKSelectorsIDs } from '../../../../../e2e/selectors/Settings/SDK.selectors';
import Icon, {
IconName,
diff --git a/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/__snapshots__/MetaMetricsAndDataCollectionSection.test.tsx.snap b/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/__snapshots__/MetaMetricsAndDataCollectionSection.test.tsx.snap
index be27f25d0a7..7c71fe2ef54 100644
--- a/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/__snapshots__/MetaMetricsAndDataCollectionSection.test.tsx.snap
+++ b/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/__snapshots__/MetaMetricsAndDataCollectionSection.test.tsx.snap
@@ -358,7 +358,7 @@ exports[`MetaMetricsAndDataCollectionSection render matches snapshot 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -366,7 +366,7 @@ exports[`MetaMetricsAndDataCollectionSection render matches snapshot 1`] = `
}
testID="metametrics-switch"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
@@ -480,7 +480,7 @@ exports[`MetaMetricsAndDataCollectionSection render matches snapshot 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -488,7 +488,7 @@ exports[`MetaMetricsAndDataCollectionSection render matches snapshot 1`] = `
}
testID="data-collection-switch"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/Settings/SecuritySettings/Sections/__snapshots__/BlockaidSettings.test.tsx.snap b/app/components/Views/Settings/SecuritySettings/Sections/__snapshots__/BlockaidSettings.test.tsx.snap
index 2857d38420c..96b5536b78f 100644
--- a/app/components/Views/Settings/SecuritySettings/Sections/__snapshots__/BlockaidSettings.test.tsx.snap
+++ b/app/components/Views/Settings/SecuritySettings/Sections/__snapshots__/BlockaidSettings.test.tsx.snap
@@ -45,7 +45,7 @@ exports[`BlockaidSettings should render correctly 1`] = `
"alignSelf": "flex-end",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -53,7 +53,7 @@ exports[`BlockaidSettings should render correctly 1`] = `
}
testID="security-alerts-toggle"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
,
diff --git a/app/components/Views/Settings/SecuritySettings/__snapshots__/SecuritySettings.test.tsx.snap b/app/components/Views/Settings/SecuritySettings/__snapshots__/SecuritySettings.test.tsx.snap
index e0e6c637858..3ef5a5f21dc 100644
--- a/app/components/Views/Settings/SecuritySettings/__snapshots__/SecuritySettings.test.tsx.snap
+++ b/app/components/Views/Settings/SecuritySettings/__snapshots__/SecuritySettings.test.tsx.snap
@@ -146,7 +146,7 @@ exports[`SecuritySettings should render correctly 1`] = `
@@ -680,7 +680,7 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-end",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -688,7 +688,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
testID="security-alerts-toggle"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -814,13 +814,13 @@ exports[`SecuritySettings should render correctly 1`] = `
"width": 51,
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
@@ -1354,7 +1354,7 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -1362,7 +1362,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
testID="display-use-safe-list-validation"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -1474,7 +1474,7 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -1482,7 +1482,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
testID="security-settings-multi-account-balances-switch"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -1563,7 +1563,7 @@ exports[`SecuritySettings should render correctly 1`] = `
style={
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 4,
"borderWidth": 0,
"padding": 16,
@@ -1657,7 +1657,7 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -1665,7 +1665,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
testID="incoming-mainnet-toggle"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -1682,7 +1682,7 @@ exports[`SecuritySettings should render correctly 1`] = `
style={
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 4,
"borderWidth": 0,
"padding": 16,
@@ -1776,7 +1776,7 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -1784,7 +1784,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
testID="incoming-linea-mainnet-toggle"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -1876,14 +1876,14 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
]
}
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -2011,7 +2011,7 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -2019,7 +2019,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
testID="nft-display-media-mode-section"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -2097,7 +2097,7 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -2105,7 +2105,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
testID="nft-opensea-autodetect-mode-section"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -2183,7 +2183,7 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -2191,7 +2191,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
testID="IPFS_GATEWAY_SECTION"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={true}
/>
@@ -2238,7 +2238,7 @@ exports[`SecuritySettings should render correctly 1`] = `
@@ -2427,7 +2427,7 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -2435,7 +2435,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
testID="metametrics-switch"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
@@ -2549,7 +2549,7 @@ exports[`SecuritySettings should render correctly 1`] = `
"alignSelf": "flex-start",
},
{
- "backgroundColor": "#bbc0c566",
+ "backgroundColor": "#BBC0C566",
"borderRadius": 16,
},
],
@@ -2557,7 +2557,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
testID="data-collection-switch"
thumbTintColor="#ffffff"
- tintColor="#bbc0c566"
+ tintColor="#BBC0C566"
value={false}
/>
diff --git a/app/components/Views/ShowDisplayMediaNFTSheet/__snapshots__/ShowDisplayNFTMediaSheet.test.tsx.snap b/app/components/Views/ShowDisplayMediaNFTSheet/__snapshots__/ShowDisplayNFTMediaSheet.test.tsx.snap
index 7d69a71c8b2..f032638081c 100644
--- a/app/components/Views/ShowDisplayMediaNFTSheet/__snapshots__/ShowDisplayNFTMediaSheet.test.tsx.snap
+++ b/app/components/Views/ShowDisplayMediaNFTSheet/__snapshots__/ShowDisplayNFTMediaSheet.test.tsx.snap
@@ -371,14 +371,14 @@ exports[`ShowNftSheet should render correctly 1`] = `
[
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
"borderWidth": 1,
"maxHeight": 1314,
"overflow": "hidden",
"paddingBottom": 0,
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -408,7 +408,7 @@ exports[`ShowNftSheet should render correctly 1`] = `
{
const chainIdDec = hexToDecimal(chainId);
diff --git a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap
index dd7b9941997..2c5c3450b99 100644
--- a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap
@@ -56,12 +56,15 @@ exports[`Wallet should render correctly 1`] = `
{
"backgroundColor": {
"alternative": "#f2f4f6",
- "alternativeHover": "#e7ebee",
- "alternativePressed": "#dbe0e6",
+ "alternativeHover": "#E7EBEE",
+ "alternativePressed": "#DBE0E6",
"default": "#ffffff",
- "defaultHover": "#f5f5f5",
- "defaultPressed": "#ebebeb",
- "hover": "#0000000a",
+ "defaultHover": "#F5F5F5",
+ "defaultPressed": "#EBEBEB",
+ "hover": "#0000000A",
+ "muted": "#f2f4f6",
+ "mutedHover": "#E7EBEE",
+ "mutedPressed": "#DBE0E6",
"pressed": "#00000014",
},
"borderBottomColor": "rgb(216, 216, 216)",
@@ -233,7 +236,7 @@ exports[`Wallet should render correctly 1`] = `
{
"alignItems": "center",
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 4,
"borderWidth": 0,
"flexDirection": "row",
@@ -779,7 +782,7 @@ exports[`Wallet should render correctly 1`] = `
-
>
diff --git a/app/components/Views/WalletActions/WalletActions.test.tsx b/app/components/Views/WalletActions/WalletActions.test.tsx
index 502541c7a97..9618ce5616f 100644
--- a/app/components/Views/WalletActions/WalletActions.test.tsx
+++ b/app/components/Views/WalletActions/WalletActions.test.tsx
@@ -15,6 +15,27 @@ import {
expectedUuid2,
MOCK_ACCOUNTS_CONTROLLER_STATE,
} from '../../../util/test/accountsControllerTestUtils';
+import useStakingChain from '../../UI/Stake/hooks/useStakingChain';
+import Engine from '../../../core/Engine';
+import { isStablecoinLendingFeatureEnabled } from '../../UI/Stake/constants';
+
+jest.mock('../../../components/UI/Stake/constants', () => ({
+ isStablecoinLendingFeatureEnabled: jest.fn(),
+}));
+
+jest.mock('../../../core/Engine', () => ({
+ context: {
+ NetworkController: {
+ setActiveNetwork: jest.fn(),
+ },
+ },
+}));
+jest.mock('../../../components/UI/Stake/hooks/useStakingChain', () => ({
+ __esModule: true,
+ default: jest.fn().mockReturnValue({
+ isStakingSupportedChain: true,
+ }),
+}));
const mockInitialState: DeepPartial = {
swaps: { '0x1': { isLive: true }, hasOnboarded: false, isLive: true },
@@ -103,6 +124,17 @@ describe('WalletActions', () => {
).toBeDefined();
});
+ it('should render earn button if the stablecoin lending feature is enabled', () => {
+ (isStablecoinLendingFeatureEnabled as jest.Mock).mockReturnValue(true);
+ const { getByTestId } = renderWithProvider(, {
+ state: mockInitialState,
+ });
+
+ expect(
+ getByTestId(WalletActionsBottomSheetSelectorsIDs.EARN_BUTTON),
+ ).toBeDefined();
+ });
+
it('should not show the buy button and swap button if the chain does not allow buying', () => {
const mockState: DeepPartial = {
swaps: { '0x1': { isLive: false }, hasOnboarded: false, isLive: true },
@@ -142,7 +174,9 @@ describe('WalletActions', () => {
state: mockState,
});
- expect(queryByTestId(WalletActionsBottomSheetSelectorsIDs.BUY_BUTTON)).toBeNull();
+ expect(
+ queryByTestId(WalletActionsBottomSheetSelectorsIDs.BUY_BUTTON),
+ ).toBeNull();
expect(
queryByTestId(WalletActionsBottomSheetSelectorsIDs.SWAP_BUTTON),
).toBeNull();
@@ -156,7 +190,9 @@ describe('WalletActions', () => {
state: mockInitialState,
});
- fireEvent.press(getByTestId(WalletActionsBottomSheetSelectorsIDs.BUY_BUTTON));
+ fireEvent.press(
+ getByTestId(WalletActionsBottomSheetSelectorsIDs.BUY_BUTTON),
+ );
expect(mockNavigate).toHaveBeenCalled();
});
@@ -165,7 +201,9 @@ describe('WalletActions', () => {
state: mockInitialState,
});
- fireEvent.press(getByTestId(WalletActionsBottomSheetSelectorsIDs.SEND_BUTTON));
+ fireEvent.press(
+ getByTestId(WalletActionsBottomSheetSelectorsIDs.SEND_BUTTON),
+ );
expect(mockNavigate).toHaveBeenCalled();
});
@@ -174,7 +212,9 @@ describe('WalletActions', () => {
state: mockInitialState,
});
- fireEvent.press(getByTestId(WalletActionsBottomSheetSelectorsIDs.SWAP_BUTTON));
+ fireEvent.press(
+ getByTestId(WalletActionsBottomSheetSelectorsIDs.SWAP_BUTTON),
+ );
expect(mockNavigate).toHaveBeenCalled();
});
@@ -183,10 +223,44 @@ describe('WalletActions', () => {
state: mockInitialState,
});
- fireEvent.press(getByTestId(WalletActionsBottomSheetSelectorsIDs.BRIDGE_BUTTON));
+ fireEvent.press(
+ getByTestId(WalletActionsBottomSheetSelectorsIDs.BRIDGE_BUTTON),
+ );
expect(mockNavigate).toHaveBeenCalled();
});
+ it('should call the onEarn function when the Earn button is pressed', () => {
+ (isStablecoinLendingFeatureEnabled as jest.Mock).mockReturnValue(true);
+ const { getByTestId } = renderWithProvider(, {
+ state: mockInitialState,
+ });
+
+ fireEvent.press(
+ getByTestId(WalletActionsBottomSheetSelectorsIDs.EARN_BUTTON),
+ );
+
+ expect(mockNavigate).toHaveBeenCalled();
+ expect(
+ Engine.context.NetworkController.setActiveNetwork,
+ ).not.toHaveBeenCalled();
+ });
+
+ it('should switch to mainnet when onEarn called on unsupported staking network', () => {
+ (isStablecoinLendingFeatureEnabled as jest.Mock).mockReturnValue(true);
+ (useStakingChain as jest.Mock).mockReturnValue({
+ isStakingSupportedChain: false,
+ });
+ const { getByTestId } = renderWithProvider(, {
+ state: mockInitialState,
+ });
+
+ fireEvent.press(
+ getByTestId(WalletActionsBottomSheetSelectorsIDs.EARN_BUTTON),
+ );
+ expect(
+ Engine.context.NetworkController.setActiveNetwork,
+ ).toHaveBeenCalledWith('mainnet');
+ });
it('disables action buttons when the account cannot sign transactions', () => {
const mockStateWithoutSigning: DeepPartial = {
...mockInitialState,
@@ -217,18 +291,30 @@ describe('WalletActions', () => {
state: mockStateWithoutSigning,
});
- const buyButton = getByTestId(WalletActionsBottomSheetSelectorsIDs.BUY_BUTTON);
- const sellButton = getByTestId(WalletActionsBottomSheetSelectorsIDs.SELL_BUTTON);
- const sendButton = getByTestId(WalletActionsBottomSheetSelectorsIDs.SEND_BUTTON);
- const swapButton = getByTestId(WalletActionsBottomSheetSelectorsIDs.SWAP_BUTTON);
+ const buyButton = getByTestId(
+ WalletActionsBottomSheetSelectorsIDs.BUY_BUTTON,
+ );
+ const sellButton = getByTestId(
+ WalletActionsBottomSheetSelectorsIDs.SELL_BUTTON,
+ );
+ const sendButton = getByTestId(
+ WalletActionsBottomSheetSelectorsIDs.SEND_BUTTON,
+ );
+ const swapButton = getByTestId(
+ WalletActionsBottomSheetSelectorsIDs.SWAP_BUTTON,
+ );
const bridgeButton = getByTestId(
WalletActionsBottomSheetSelectorsIDs.BRIDGE_BUTTON,
);
+ const earnButton = getByTestId(
+ WalletActionsBottomSheetSelectorsIDs.EARN_BUTTON,
+ );
expect(buyButton.props.disabled).toBe(true);
expect(sellButton.props.disabled).toBe(true);
expect(sendButton.props.disabled).toBe(true);
expect(swapButton.props.disabled).toBe(true);
expect(bridgeButton.props.disabled).toBe(true);
+ expect(earnButton.props.disabled).toBe(true);
});
});
diff --git a/app/components/Views/WalletActions/WalletActions.tsx b/app/components/Views/WalletActions/WalletActions.tsx
index 30dd0cfd47b..54ff78139c4 100644
--- a/app/components/Views/WalletActions/WalletActions.tsx
+++ b/app/components/Views/WalletActions/WalletActions.tsx
@@ -40,6 +40,9 @@ import {
} from '../../UI/Ramp/routes/utils';
import { selectCanSignTransactions } from '../../../selectors/accountsController';
import { WalletActionType } from '../../UI/WalletAction/WalletAction.types';
+import Engine from '../../../core/Engine';
+import useStakingChain from '../../UI/Stake/hooks/useStakingChain';
+import { isStablecoinLendingFeatureEnabled } from '../../UI/Stake/constants';
const WalletActions = () => {
const { styles } = useStyles(styleSheet, {});
@@ -51,7 +54,7 @@ const WalletActions = () => {
const ticker = useSelector(selectTicker);
const swapsIsLive = useSelector(swapsLivenessSelector);
const dispatch = useDispatch();
-
+ const { isStakingSupportedChain } = useStakingChain();
const [isNetworkRampSupported] = useRampNetwork();
const { trackEvent, createEventBuilder } = useMetrics();
@@ -89,6 +92,32 @@ const WalletActions = () => {
createEventBuilder,
]);
+ const onEarn = useCallback(async () => {
+ if (!isStakingSupportedChain) {
+ await Engine.context.NetworkController.setActiveNetwork('mainnet');
+ }
+
+ closeBottomSheetAndNavigate(() => {
+ navigate('StakeScreens', { screen: Routes.STAKING.STAKE });
+ });
+ trackEvent(
+ createEventBuilder(MetaMetricsEvents.EARN_BUTTON_CLICKED)
+ .addProperties({
+ text: 'Earn',
+ location: 'TabBar',
+ chain_id_destination: getDecimalChainId(chainId),
+ })
+ .build(),
+ );
+ }, [
+ closeBottomSheetAndNavigate,
+ navigate,
+ chainId,
+ createEventBuilder,
+ trackEvent,
+ isStakingSupportedChain,
+ ]);
+
const onBuy = useCallback(() => {
closeBottomSheetAndNavigate(() => {
navigate(...createBuyNavigationDetails());
@@ -222,7 +251,6 @@ const WalletActions = () => {
/>
)}
{AppConstants.SWAPS.ACTIVE &&
- swapsIsLive &&
isSwapsAllowed(chainId) && (
{
actionID={WalletActionsBottomSheetSelectorsIDs.SWAP_BUTTON}
iconStyle={styles.icon}
iconSize={AvatarSize.Md}
- disabled={!canSignTransactions}
+ disabled={!canSignTransactions || !swapsIsLive}
/>
)}
{isBridgeAllowed(chainId) && (
@@ -263,6 +291,17 @@ const WalletActions = () => {
iconSize={AvatarSize.Md}
disabled={false}
/>
+ {isStablecoinLendingFeatureEnabled() && (
+
+ )}
);
diff --git a/app/components/Views/WalletConnectSessions/__snapshots__/index.test.tsx.snap b/app/components/Views/WalletConnectSessions/__snapshots__/index.test.tsx.snap
index 1429eb1fc7d..a58081bfae1 100644
--- a/app/components/Views/WalletConnectSessions/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/WalletConnectSessions/__snapshots__/index.test.tsx.snap
@@ -1196,7 +1196,7 @@ exports[`WalletConnectSessions should render active sessions 1`] = `
onLongPress={[Function]}
style={
{
- "borderBottomColor": "#bbc0c566",
+ "borderBottomColor": "#BBC0C566",
"borderBottomWidth": 1,
"flexDirection": "row",
"paddingHorizontal": 20,
@@ -1313,7 +1313,7 @@ exports[`WalletConnectSessions should render active sessions 1`] = `
onLongPress={[Function]}
style={
{
- "borderBottomColor": "#bbc0c566",
+ "borderBottomColor": "#BBC0C566",
"borderBottomWidth": 1,
"flexDirection": "row",
"paddingHorizontal": 20,
diff --git a/app/components/Views/confirmations/Approval/__snapshots__/index.test.tsx.snap b/app/components/Views/confirmations/Approval/__snapshots__/index.test.tsx.snap
index 2ced70357f9..d74d9b1a64a 100644
--- a/app/components/Views/confirmations/Approval/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/confirmations/Approval/__snapshots__/index.test.tsx.snap
@@ -617,7 +617,7 @@ exports[`Approval render matches snapshot 1`] = `
style={
{
"alignItems": "center",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 12,
"borderWidth": 1,
"color": "#141618",
@@ -659,7 +659,7 @@ exports[`Approval render matches snapshot 1`] = `
style={
[
{
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderRadius": 8,
"borderWidth": 1,
"padding": 16,
@@ -1025,7 +1025,7 @@ exports[`Approval render matches snapshot 1`] = `
style={
{
"alignItems": "center",
- "backgroundColor": "#d738471a",
+ "backgroundColor": "#D738471A",
"borderColor": "#d73847",
"borderRadius": 8,
"borderWidth": 1,
@@ -1349,7 +1349,7 @@ exports[`Approval render matches snapshot 1`] = `
transactionMeta.id === transaction.id,
diff --git a/app/components/Views/confirmations/SendFlow/AddressFrom/__snapshots__/AddressFrom.test.tsx.snap b/app/components/Views/confirmations/SendFlow/AddressFrom/__snapshots__/AddressFrom.test.tsx.snap
index 3fd50ae8989..109d5f44708 100644
--- a/app/components/Views/confirmations/SendFlow/AddressFrom/__snapshots__/AddressFrom.test.tsx.snap
+++ b/app/components/Views/confirmations/SendFlow/AddressFrom/__snapshots__/AddressFrom.test.tsx.snap
@@ -39,7 +39,7 @@ exports[`SendFlowAddressFrom should render correctly 1`] = `
style={
[
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 8,
"borderWidth": 1,
"flex": 1,
@@ -50,7 +50,7 @@ exports[`SendFlowAddressFrom should render correctly 1`] = `
"padding": 10,
},
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
},
]
}
diff --git a/app/components/Views/confirmations/SendFlow/AddressTo/__snapshots__/AddressTo.test.tsx.snap b/app/components/Views/confirmations/SendFlow/AddressTo/__snapshots__/AddressTo.test.tsx.snap
index 37e775db1db..c41fc133e55 100644
--- a/app/components/Views/confirmations/SendFlow/AddressTo/__snapshots__/AddressTo.test.tsx.snap
+++ b/app/components/Views/confirmations/SendFlow/AddressTo/__snapshots__/AddressTo.test.tsx.snap
@@ -49,7 +49,7 @@ exports[`SendFlowAddressTo should render correctly 1`] = `
"paddingHorizontal": 10,
},
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
},
]
}
diff --git a/app/components/Views/confirmations/SendFlow/Amount/__snapshots__/index.test.tsx.snap b/app/components/Views/confirmations/SendFlow/Amount/__snapshots__/index.test.tsx.snap
index 2141b9fedb0..efcacffe66c 100644
--- a/app/components/Views/confirmations/SendFlow/Amount/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/confirmations/SendFlow/Amount/__snapshots__/index.test.tsx.snap
@@ -6059,7 +6059,7 @@ exports[`Amount should render correctly 1`] = `
"paddingVertical": 8,
},
{
- "backgroundColor": "#bf52081a",
+ "backgroundColor": "#BF52081A",
"borderColor": "#bf5208",
},
{
@@ -6769,7 +6769,7 @@ exports[`Amount should show a warning when conversion rate is not available 1`]
"paddingVertical": 8,
},
{
- "backgroundColor": "#bf52081a",
+ "backgroundColor": "#BF52081A",
"borderColor": "#bf5208",
},
{
@@ -7737,7 +7737,7 @@ exports[`Amount should show an error message if balance is insufficient 1`] = `
style={
{
"alignItems": "center",
- "backgroundColor": "#d738471a",
+ "backgroundColor": "#D738471A",
"borderColor": "#d73847",
"borderRadius": 8,
"borderWidth": 1,
diff --git a/app/components/Views/confirmations/SendFlow/Amount/index.js b/app/components/Views/confirmations/SendFlow/Amount/index.js
index fee467612f8..0378afb6fb2 100644
--- a/app/components/Views/confirmations/SendFlow/Amount/index.js
+++ b/app/components/Views/confirmations/SendFlow/Amount/index.js
@@ -98,7 +98,10 @@ import { withMetricsAwareness } from '../../../../../components/hooks/useMetrics
import { selectGasFeeEstimates } from '../../../../../selectors/confirmTransaction';
import { selectGasFeeControllerEstimateType } from '../../../../../selectors/gasFeeController';
import { createBuyNavigationDetails } from '../../../../UI/Ramp/routes/utils';
-import { selectNativeCurrencyByChainId, selectProviderTypeByChainId } from '../../../../../selectors/networkController';
+import {
+ selectNativeCurrencyByChainId,
+ selectProviderTypeByChainId,
+} from '../../../../../selectors/networkController';
import { selectContractExchangeRatesByChainId } from '../../../../../selectors/tokenRatesController';
const KEYBOARD_OFFSET = Device.isSmallDevice() ? 80 : 120;
@@ -1037,14 +1040,14 @@ class Amount extends PureComponent {
};
handleSelectedAssetBalance = (
- { address, decimals, symbol, isETH },
+ { address, decimals, symbol, isETH, isNative },
renderableBalance,
) => {
const { accounts, selectedAddress, contractBalances } = this.props;
let currentBalance;
if (renderableBalance) {
currentBalance = `${renderableBalance} ${symbol}`;
- } else if (isETH) {
+ } else if (isETH || isNative) {
currentBalance = `${renderFromWei(
accounts[selectedAddress].balance,
)} ${symbol}`;
@@ -1586,7 +1589,7 @@ const mapStateToProps = (state, ownProps) => {
chainId,
networkClientId,
};
-}
+};
const mapDispatchToProps = (dispatch) => ({
setTransactionObject: (transaction) =>
diff --git a/app/components/Views/confirmations/SendFlow/Confirm/__snapshots__/index.test.tsx.snap b/app/components/Views/confirmations/SendFlow/Confirm/__snapshots__/index.test.tsx.snap
index e2636ab25ca..a95c7c6447e 100644
--- a/app/components/Views/confirmations/SendFlow/Confirm/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/confirmations/SendFlow/Confirm/__snapshots__/index.test.tsx.snap
@@ -469,7 +469,7 @@ exports[`Confirm should render correctly 1`] = `
style={
{
"backgroundColor": "#ffffff",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 4,
"borderWidth": 1,
"flexDirection": "row",
@@ -699,7 +699,7 @@ exports[`Confirm should render correctly 1`] = `
"height": 32,
"justifyContent": "center",
"overflow": "hidden",
- "shadowColor": "#0000001a",
+ "shadowColor": "#0000001A",
"shadowOffset": {
"height": 2,
"width": 0,
@@ -863,7 +863,7 @@ exports[`Confirm should render correctly 1`] = `
"paddingHorizontal": 10,
},
{
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
},
]
}
@@ -1207,7 +1207,7 @@ exports[`Confirm should render correctly 1`] = `
style={
[
{
- "borderColor": "#bbc0c566",
+ "borderColor": "#BBC0C566",
"borderRadius": 8,
"borderWidth": 1,
"padding": 16,
@@ -1486,7 +1486,7 @@ exports[`Confirm should render correctly 1`] = `
style={
[
{
- "borderBottomColor": "#bbc0c566",
+ "borderBottomColor": "#BBC0C566",
"borderBottomWidth": 1,
"marginVertical": 6,
},
diff --git a/app/components/Views/confirmations/SendFlow/Confirm/components/CustomGasModal/__snapshots__/CustomGasModal.test.tsx.snap b/app/components/Views/confirmations/SendFlow/Confirm/components/CustomGasModal/__snapshots__/CustomGasModal.test.tsx.snap
index fb4545c66da..9c415e7612f 100644
--- a/app/components/Views/confirmations/SendFlow/Confirm/components/CustomGasModal/__snapshots__/CustomGasModal.test.tsx.snap
+++ b/app/components/Views/confirmations/SendFlow/Confirm/components/CustomGasModal/__snapshots__/CustomGasModal.test.tsx.snap
@@ -384,7 +384,7 @@ exports[`CustomGasModal should render correctly 1`] = `
style={
{
"alignItems": "center",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 6,
"borderWidth": 1,
"flexDirection": "row",
@@ -760,7 +760,7 @@ exports[`CustomGasModal should render correctly 1`] = `
style={
{
"alignItems": "center",
- "borderColor": "#bbc0c5",
+ "borderColor": "#848c96",
"borderRadius": 6,
"borderWidth": 1,
"flexDirection": "row",
diff --git a/app/components/Views/confirmations/SendFlow/Confirm/index.js b/app/components/Views/confirmations/SendFlow/Confirm/index.js
index ba28b91a446..79d968adc0d 100644
--- a/app/components/Views/confirmations/SendFlow/Confirm/index.js
+++ b/app/components/Views/confirmations/SendFlow/Confirm/index.js
@@ -134,7 +134,7 @@ import {
// Pending updated multichain UX to specify the send chain.
// eslint-disable-next-line no-restricted-syntax
selectNetworkClientId,
- selectProviderTypeByChainId
+ selectProviderTypeByChainId,
} from '../../../../../selectors/networkController';
import { selectContractExchangeRatesByChainId } from '../../../../../selectors/tokenRatesController';
import { updateTransactionToMaxValue } from './utils';
@@ -472,7 +472,7 @@ class Confirm extends PureComponent {
navigation,
providerType,
isPaymentRequest,
- setTransactionId
+ setTransactionId,
} = this.props;
const {
@@ -743,7 +743,8 @@ class Confirm extends PureComponent {
let transactionValue, transactionValueFiat;
const valueBN = hexToBN(value);
- const parsedTicker = getTicker(ticker);
+ const symbol = ticker ?? selectedAsset?.symbol;
+ const parsedTicker = getTicker(symbol);
if (selectedAsset.isETH) {
transactionValue = `${renderFromWei(value)} ${parsedTicker}`;
@@ -1617,8 +1618,7 @@ const mapStateToProps = (state) => {
),
shouldUseSmartTransaction: selectShouldUseSmartTransaction(state),
transactionMetricsById: selectTransactionMetrics(state),
- transactionMetadata:
- selectCurrentTransactionMetadata(state),
+ transactionMetadata: selectCurrentTransactionMetadata(state),
useTransactionSimulations: selectUseTransactionSimulations(state),
securityAlertResponse: selectCurrentTransactionSecurityAlertResponse(state),
};
diff --git a/app/components/Views/confirmations/SendFlow/ErrorMessage/__snapshots__/index.test.tsx.snap b/app/components/Views/confirmations/SendFlow/ErrorMessage/__snapshots__/index.test.tsx.snap
index 1163afc4e32..807f74fd070 100644
--- a/app/components/Views/confirmations/SendFlow/ErrorMessage/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/confirmations/SendFlow/ErrorMessage/__snapshots__/index.test.tsx.snap
@@ -13,7 +13,7 @@ exports[`ErrorMessage should render correctly 1`] = `
},
undefined,
{
- "backgroundColor": "#d738471a",
+ "backgroundColor": "#D738471A",
"borderColor": "#d73847",
},
undefined,
diff --git a/app/components/Views/confirmations/SendFlow/SendTo/__snapshots__/index.test.tsx.snap b/app/components/Views/confirmations/SendFlow/SendTo/__snapshots__/index.test.tsx.snap
index c5b2ba23d38..d5c7c223cbd 100644
--- a/app/components/Views/confirmations/SendFlow/SendTo/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/confirmations/SendFlow/SendTo/__snapshots__/index.test.tsx.snap
@@ -18,7 +18,7 @@ exports[`SendTo Component should render 1`] = `
;
reason: Reason;
req?: Record;
- result_type: ResultType;
+ result_type: BlockaidResultType;
source?: SecurityAlertSource;
}
diff --git a/app/components/Views/confirmations/components/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap b/app/components/Views/confirmations/components/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap
index 515328ab1ae..2a73b7a46bd 100644
--- a/app/components/Views/confirmations/components/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap
+++ b/app/components/Views/confirmations/components/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap
@@ -28,7 +28,7 @@ exports[`BlockaidBanner should render correctly 1`] = `
}
style={
{
- "backgroundColor": "#bf52081a",
+ "backgroundColor": "#BF52081A",
"borderColor": "#bf5208",
"borderLeftWidth": 4,
"borderRadius": 4,
@@ -181,7 +181,7 @@ exports[`BlockaidBanner should render correctly with list attack details 1`] = `
}
style={
{
- "backgroundColor": "#d738471a",
+ "backgroundColor": "#D738471A",
"borderColor": "#d73847",
"borderLeftWidth": 4,
"borderRadius": 4,
@@ -334,7 +334,7 @@ exports[`BlockaidBanner should render correctly with reason "raw_signature_farmi
}
style={
{
- "backgroundColor": "#d738471a",
+ "backgroundColor": "#D738471A",
"borderColor": "#d73847",
"borderLeftWidth": 4,
"borderRadius": 4,
@@ -496,7 +496,7 @@ exports[`BlockaidBanner should render normal banner alert if resultType is faile
= {
+ [Field.Deadline]: [...PRIMARY_TYPES_PERMIT],
+ [Field.EndTime]: [...PRIMARY_TYPES_ORDER],
+ [Field.Expiration]: [PrimaryType.PermitBatch, PrimaryType.PermitSingle],
+ [Field.Expiry]: [...PRIMARY_TYPES_PERMIT],
+ [Field.SigDeadline]: [...PRIMARY_TYPES_PERMIT],
+ [Field.StartTime]: [...PRIMARY_TYPES_ORDER],
+ [Field.ValidTo]: [...PRIMARY_TYPES_ORDER],
+};
+
+function isDateField(label: string, primaryType?: PrimaryType) {
+ return (FIELD_DATE_PRIMARY_TYPES[label] || [])?.includes(primaryType || '');
+}
+
const createStyles = (depth: number) =>
StyleSheet.create({
container: {
@@ -23,25 +61,41 @@ const createStyles = (depth: number) =>
const DataField = memo(
({
+ chainId,
+ depth,
label,
+ primaryType,
type,
value,
- chainId,
- depth,
}: {
+ chainId: string;
+ depth: number;
label: string;
+ primaryType?: PrimaryType;
type: string;
value: string;
- chainId: string;
- depth: number;
}) => {
const styles = createStyles(depth);
let fieldDisplay;
if (type === 'address' && isValidHexAddress(value as Hex)) {
fieldDisplay = ;
+ } else if (isDateField(label, primaryType) && Boolean(value)) {
+ const intValue = parseInt(value, 10);
+
+ fieldDisplay =
+ intValue === NONE_DATE_VALUE ? (
+ {strings('confirm.none')}
+ ) : (
+
+ );
} else if (typeof value === 'object' && value !== null) {
fieldDisplay = (
-
+
);
} else {
fieldDisplay = {value};
diff --git a/app/components/Views/confirmations/components/Confirm/DataTree/DataTree.test.tsx b/app/components/Views/confirmations/components/Confirm/DataTree/DataTree.test.tsx
index f3ea36c677c..8cf8288c8ce 100644
--- a/app/components/Views/confirmations/components/Confirm/DataTree/DataTree.test.tsx
+++ b/app/components/Views/confirmations/components/Confirm/DataTree/DataTree.test.tsx
@@ -3,6 +3,10 @@ import React from 'react';
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import { backgroundState } from '../../../../../../util/test/initial-root-state';
import DataTree, { DataTreeInput } from './DataTree';
+import { PrimaryTypeOrder } from '../../../constants/signatures';
+import { NONE_DATE_VALUE } from '../../../utils/date';
+
+const timestamp = 1647359825; // March 15, 2022 15:57:05 UTC
const mockSanitizedTypedSignV3Message = {
from: {
@@ -25,12 +29,20 @@ const mockSanitizedTypedSignV3Message = {
},
type: 'Person',
},
+ endTime: {
+ value: timestamp.toString(),
+ type: 'uint256',
+ },
+ startTime: {
+ value: NONE_DATE_VALUE,
+ type: 'uint256',
+ },
contents: { value: 'Hello, Bob!', type: 'string' },
};
describe('NoChangeSimulation', () => {
- it('should display types sign v1 message correctly', async () => {
- const { getByText } = renderWithProvider(
+ it('displays types sign v1 message', async () => {
+ const { getByText, queryByText } = renderWithProvider(
{
expect(getByText('Hi, Alice!')).toBeDefined();
expect(getByText('A Number')).toBeDefined();
expect(getByText('1337')).toBeDefined();
+ // date field not supported for v1
+ expect(queryByText('15 March 2022, 15:57')).toBeNull();
});
- it('should display types sign v3/v4 message correctly', async () => {
+ it('displays types sign v3/v4 message', async () => {
const { getByText, getAllByText } = renderWithProvider(
,
{
state: {
@@ -72,5 +87,8 @@ describe('NoChangeSimulation', () => {
expect(getByText('Cow')).toBeDefined();
expect(getByText('To')).toBeDefined();
expect(getByText('Bob')).toBeDefined();
+ // date field displayed for permit types
+ expect(getByText('15 March 2022, 15:57')).toBeDefined();
+ expect(getByText('None')).toBeDefined();
});
});
diff --git a/app/components/Views/confirmations/components/Confirm/DataTree/DataTree.tsx b/app/components/Views/confirmations/components/Confirm/DataTree/DataTree.tsx
index 5eba7b98ccf..63664d6b6bd 100644
--- a/app/components/Views/confirmations/components/Confirm/DataTree/DataTree.tsx
+++ b/app/components/Views/confirmations/components/Confirm/DataTree/DataTree.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
+import { PrimaryType } from '../../../constants/signatures';
import DataField from './DataField';
export type DataTreeInput = Record;
@@ -16,20 +17,23 @@ const DataTree = ({
data,
chainId,
depth = 0,
+ primaryType,
}: {
data: DataTreeInput;
chainId: string;
depth?: number;
+ primaryType?: PrimaryType;
}) => (
{Object.keys(data).map((dataKey: string, index: number) => {
const datum = data[dataKey];
return (
);
diff --git a/app/components/Views/confirmations/components/Confirm/Footer/Footer.tsx b/app/components/Views/confirmations/components/Confirm/Footer/Footer.tsx
index bbcbc7bb052..fdb41467912 100644
--- a/app/components/Views/confirmations/components/Confirm/Footer/Footer.tsx
+++ b/app/components/Views/confirmations/components/Confirm/Footer/Footer.tsx
@@ -9,9 +9,9 @@ import Button, {
ButtonWidthTypes,
} from '../../../../../../component-library/components/Buttons/Button';
import { useStyles } from '../../../../../../component-library/hooks';
+import { ResultType } from '../../../constants/signatures';
import { useConfirmActions } from '../../../hooks/useConfirmActions';
import { useSecurityAlertResponse } from '../../../hooks/useSecurityAlertResponse';
-import { ResultType } from '../../BlockaidBanner/BlockaidBanner.types';
import styleSheet from './Footer.styles';
const Footer = () => {
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Message.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Message.tsx
index 3b70d40286b..93ca51bbc5d 100644
--- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Message.tsx
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Message.tsx
@@ -6,6 +6,7 @@ import { parseSanitizeTypedDataMessage } from '../../../../utils/signatures';
import { strings } from '../../../../../../../../locales/i18n';
import { useSignatureRequest } from '../../../../hooks/useSignatureRequest';
import { useStyles } from '../../../../../../../component-library/hooks';
+import { useTypedSignSimulationEnabled } from '../../../../hooks/useTypedSignSimulationEnabled';
import InfoRow from '../../../UI/InfoRow';
import DataTree from '../../DataTree';
import SignatureMessageSection from '../../SignatureMessageSection';
@@ -14,6 +15,7 @@ import styleSheet from './Message.styles';
const Message = () => {
const signatureRequest = useSignatureRequest();
+ const isSimulationSupported = useTypedSignSimulationEnabled();
const chainId = signatureRequest?.chainId as Hex;
const { styles } = useStyles(styleSheet, {});
@@ -31,12 +33,14 @@ const Message = () => {
return (
- {primaryType}
-
+ isSimulationSupported ? undefined : (
+
+ {primaryType}
+
+ )
}
messageExpanded={
@@ -50,6 +54,7 @@ const Message = () => {
}
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.test.tsx
index b02a9f7a8dd..55d916500e0 100644
--- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.test.tsx
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.test.tsx
@@ -1,10 +1,22 @@
import React from 'react';
import renderWithProvider from '../../../../../../../util/test/renderWithProvider';
-import { typedSignV3ConfirmationState } from '../../../../../../../util/test/confirm-data-helpers';
+import {
+ typedSignV3ConfirmationState,
+ typedSignV4ConfirmationState,
+} from '../../../../../../../util/test/confirm-data-helpers';
import TypedSignV3V4 from './TypedSignV3V4';
import { fireEvent } from '@testing-library/react-native';
+jest.mock('../../../../../../../core/Engine', () => ({
+ resetState: jest.fn(),
+ context: {
+ NetworkController: {
+ findNetworkClientIdByChainId: () => 123,
+ },
+ },
+}));
+
describe('TypedSignV3V4', () => {
it('should contained required text', async () => {
const { getByText } = renderWithProvider(, {
@@ -17,6 +29,17 @@ describe('TypedSignV3V4', () => {
expect(getByText('Mail')).toBeDefined();
});
+ it('should not display primaty type if simulation section is displayed', async () => {
+ const { getByText, queryByText } = renderWithProvider(, {
+ state: typedSignV4ConfirmationState,
+ });
+ expect(getByText('Request from')).toBeDefined();
+ expect(getByText('metamask.github.io')).toBeDefined();
+ expect(getByText('Message')).toBeDefined();
+ expect(queryByText('Primary type')).toBeNull();
+ expect(queryByText('Mail')).toBeNull();
+ });
+
it('should show detailed message when message section is clicked', async () => {
const { getByText, getAllByText } = renderWithProvider(, {
state: typedSignV3ConfirmationState,
diff --git a/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx b/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx
deleted file mode 100644
index 838361aee88..00000000000
--- a/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react';
-
-import renderWithProvider from '../../../../../../util/test/renderWithProvider';
-import { personalSignatureConfirmationState } from '../../../../../../util/test/confirm-data-helpers';
-import NoChangeSimulation from './NoChangeSimulation';
-
-describe('NoChangeSimulation', () => {
- it('should render text correctly', async () => {
- const { getByText } = renderWithProvider(, {
- state: personalSignatureConfirmationState,
- });
- expect(getByText('Estimated changes')).toBeDefined();
- expect(
- getByText(
- "You're signing into a site and there are no predicted changes to your account."
- ),
- ).toBeDefined();
- });
-
- it('should return null if preference useTransactionSimulations is not enabled', async () => {
- const { queryByText } = renderWithProvider(, {
- state: {
- engine: {
- backgroundState: {
- ...personalSignatureConfirmationState,
- PreferencesController: {
- ...personalSignatureConfirmationState.engine.backgroundState
- .PreferencesController,
- useTransactionSimulations: false,
- },
- },
- },
- },
- });
- expect(queryByText('Estimated changes')).toBeNull();
- });
-});
diff --git a/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.tsx b/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.tsx
deleted file mode 100644
index a62bf20c7ca..00000000000
--- a/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-import { useSelector } from 'react-redux';
-
-import { strings } from '../../../../../../../locales/i18n';
-import { selectUseTransactionSimulations } from '../../../../../../selectors/preferencesController';
-import InfoSection from '../../UI/InfoRow/InfoSection';
-import InfoRow from '../../UI/InfoRow';
-
-// todo: this component can be deleted if not used anywhere
-const NoChangeSimulation = () => {
- const useTransactionSimulations = useSelector(
- selectUseTransactionSimulations,
- );
-
- if (useTransactionSimulations !== true) {
- return null;
- }
-
- return (
-
-
- {strings('confirm.simulation.personal_sign_info')}
-
-
- );
-};
-
-export default NoChangeSimulation;
diff --git a/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/index.ts b/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/index.ts
deleted file mode 100644
index 4221a223fe2..00000000000
--- a/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './NoChangeSimulation';
diff --git a/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.tsx b/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.tsx
index 0f1706aab1e..3ff98b4f643 100644
--- a/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.tsx
+++ b/app/components/Views/confirmations/components/Confirm/SignatureMessageSection/SignatureMessageSection.tsx
@@ -10,7 +10,7 @@ import { IconVerticalPosition } from '../../UI/ExpandableSection/ExpandableSecti
import styleSheet from './SignatureMessageSection.styles';
interface SignatureMessageSectionProps {
- messageCollapsed: ReactNode | string;
+ messageCollapsed?: ReactNode | string;
messageExpanded: ReactNode;
copyMessageText: string;
}
@@ -27,15 +27,17 @@ const SignatureMessageSection = ({
collapsedContent={
{strings('confirm.message')}
-
- {typeof messageCollapsed === 'string' ? (
-
- {messageCollapsed}
-
- ) : (
- messageCollapsed
- )}
-
+ {messageCollapsed && (
+
+ {typeof messageCollapsed === 'string' ? (
+
+ {messageCollapsed}
+
+ ) : (
+ messageCollapsed
+ )}
+
+ )}
}
expandedContent={
diff --git a/app/components/Views/confirmations/components/CustomNonce/__snapshots__/index.test.tsx.snap b/app/components/Views/confirmations/components/CustomNonce/__snapshots__/index.test.tsx.snap
index a08fd7fcb33..caf1abdb4a7 100644
--- a/app/components/Views/confirmations/components/CustomNonce/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/confirmations/components/CustomNonce/__snapshots__/index.test.tsx.snap
@@ -4,7 +4,7 @@ exports[`CustomNonce should render correctly 1`] = `
{
+ it('renders date', async () => {
+ const { getByText } = render();
+ expect(getByText('15 March 2022, 15:57')).toBeDefined();
+ });
+});
diff --git a/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/InfoDate/InfoDate.tsx b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/InfoDate/InfoDate.tsx
new file mode 100644
index 00000000000..3c833bdbac7
--- /dev/null
+++ b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/InfoDate/InfoDate.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Text } from 'react-native';
+
+import { formatUTCDateFromUnixTimestamp } from '../../../../../utils/date';
+
+interface InfoDateProps {
+ unixTimestamp: number;
+}
+
+const InfoDate = ({ unixTimestamp }: InfoDateProps) => (
+ {formatUTCDateFromUnixTimestamp(unixTimestamp)}
+);
+
+export default InfoDate;
diff --git a/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/InfoDate/index.ts b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/InfoDate/index.ts
new file mode 100644
index 00000000000..c6148f66f1c
--- /dev/null
+++ b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/InfoDate/index.ts
@@ -0,0 +1 @@
+export { default } from './InfoDate';
diff --git a/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.stories.tsx b/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.stories.tsx
new file mode 100644
index 00000000000..e69c55c8ee1
--- /dev/null
+++ b/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.stories.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react-native';
+import { StyleProp, Text, TextStyle, View } from 'react-native';
+
+import TextWithTooltip from '.';
+
+const style = {
+ container: { padding: 8 },
+ title: { marginTop: 20, fontSize: 20, fontWeight: '700' },
+};
+
+storiesOf('Confirmations / TextWithTooltip', module)
+ .addDecorator((getStory) => getStory())
+ .add('Default', () => (
+
+ }>
+ Simple Text With Tooltip
+
+
+
+ ));
diff --git a/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.styles.ts b/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.styles.ts
new file mode 100644
index 00000000000..d4aa4e8f379
--- /dev/null
+++ b/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.styles.ts
@@ -0,0 +1,31 @@
+import { StyleSheet } from 'react-native';
+
+import { Theme } from '../../../../../../util/theme/models';
+import { fontStyles } from '../../../../../../styles/common';
+
+const styleSheet = (params: { theme: Theme }) => {
+ const { theme } = params;
+
+ return StyleSheet.create({
+ container: {
+ backgroundColor: theme.colors.background.default,
+ paddingHorizontal: 8,
+ paddingVertical: 8,
+ },
+ tooltipText: {
+ fontSize: 16,
+ ...fontStyles.normal,
+ },
+ tooltipHeader: {
+ paddingHorizontal: 8,
+ paddingVertical: 8,
+ },
+ tooltipContext: {
+ paddingHorizontal: 40,
+ paddingTop: 40,
+ paddingBottom: 56,
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.test.tsx b/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.test.tsx
new file mode 100644
index 00000000000..cf7685008d5
--- /dev/null
+++ b/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.test.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { fireEvent, render } from '@testing-library/react-native';
+
+import TextWithTooltip from './TextWithTooltip';
+
+describe('TextWithTooltip', () => {
+ it('renders correctly', async () => {
+ const { getByText } = render(
+ ,
+ );
+ expect(getByText('some_dummy_value')).toBeDefined();
+ });
+
+ it('should open modal when value pressed', async () => {
+ const { getByTestId, getByText } = render(
+ ,
+ );
+ expect(getByText('some_dummy_value')).toBeDefined();
+ fireEvent.press(getByText('some_dummy_value'));
+ expect(getByText('some_dummy_tooltip')).toBeDefined();
+ fireEvent.press(getByTestId('tooltipTestId'));
+ expect(getByText('some_dummy_value')).toBeDefined();
+ });
+});
diff --git a/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.tsx b/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.tsx
new file mode 100644
index 00000000000..7420dfc2b23
--- /dev/null
+++ b/app/components/Views/confirmations/components/UI/TextWithTooltip/TextWithTooltip.tsx
@@ -0,0 +1,55 @@
+import React, { useState } from 'react';
+import { Text, TouchableOpacity, View } from 'react-native';
+
+import BottomModal from '../BottomModal';
+import { useStyles } from '../../../../../hooks/useStyles';
+import styleSheet from './TextWithTooltip.styles';
+import ButtonIcon from '../../../../../../component-library/components/Buttons/ButtonIcon';
+import { ButtonIconSizes } from '../../../../../../component-library/components/Buttons/ButtonIcon/ButtonIcon.types';
+import {
+ IconColor,
+ IconName,
+} from '../../../../../../component-library/components/Icons/Icon';
+
+interface TextWithTooltipProps {
+ text: string;
+ tooltip: string;
+ tooltipTestId?: string;
+}
+
+const TextWithTooltip = ({
+ text,
+ tooltip,
+ tooltipTestId,
+}: TextWithTooltipProps) => {
+ const [isTooltipVisible, setTooltipVisible] = useState(false);
+ const { styles } = useStyles(styleSheet, {});
+
+ return (
+
+ setTooltipVisible(true)}>
+ {text}
+
+ {isTooltipVisible && (
+ setTooltipVisible(false)}>
+
+
+ setTooltipVisible(false)}
+ iconName={IconName.ArrowLeft}
+ testID={tooltipTestId ?? 'tooltipTestId'}
+ />
+
+
+ {tooltip}
+
+
+
+ )}
+
+ );
+};
+
+export default TextWithTooltip;
diff --git a/app/components/Views/confirmations/components/UI/TextWithTooltip/index.ts b/app/components/Views/confirmations/components/UI/TextWithTooltip/index.ts
new file mode 100644
index 00000000000..61aa67bd5d8
--- /dev/null
+++ b/app/components/Views/confirmations/components/UI/TextWithTooltip/index.ts
@@ -0,0 +1 @@
+export { default } from './TextWithTooltip';
diff --git a/app/components/Views/confirmations/constants/signatures.ts b/app/components/Views/confirmations/constants/signatures.ts
index 79a6dfdf184..6b501dec49c 100644
--- a/app/components/Views/confirmations/constants/signatures.ts
+++ b/app/components/Views/confirmations/constants/signatures.ts
@@ -34,3 +34,13 @@ export const PRIMARY_TYPES_ORDER: PrimaryTypeOrder[] =
export const PRIMARY_TYPES_PERMIT: PrimaryTypePermit[] =
Object.values(PrimaryTypePermit);
export const PRIMARY_TYPES: PrimaryType[] = Object.values(PrimaryType);
+
+export enum ResultType {
+ Benign = 'Benign',
+ Malicious = 'Malicious',
+ Warning = 'Warning',
+
+ // MetaMask defined result types
+ Failed = 'Failed',
+ RequestInProgress = 'RequestInProgress',
+}
diff --git a/app/components/Views/confirmations/hooks/useConfirmActions.test.ts b/app/components/Views/confirmations/hooks/useConfirmActions.test.ts
index 31a512d6997..b646820b915 100644
--- a/app/components/Views/confirmations/hooks/useConfirmActions.test.ts
+++ b/app/components/Views/confirmations/hooks/useConfirmActions.test.ts
@@ -1,6 +1,7 @@
import Engine from '../../../../core/Engine';
import { renderHookWithProvider } from '../../../../util/test/renderWithProvider';
import { personalSignatureConfirmationState } from '../../../../util/test/confirm-data-helpers';
+import PPOMUtil from '../../../../lib/ppom/ppom-util';
import { useConfirmActions } from './useConfirmActions';
jest.mock('../../../../core/Engine', () => ({
@@ -23,6 +24,10 @@ describe('useConfirmAction', () => {
});
it('call required callbacks when confirm button is clicked', async () => {
+ const clearSecurityAlertResponseSpy = jest.spyOn(
+ PPOMUtil,
+ 'clearSignatureSecurityAlertResponse',
+ );
const { result } = renderHookWithProvider(() => useConfirmActions(), {
state: personalSignatureConfirmationState,
});
@@ -30,14 +35,20 @@ describe('useConfirmAction', () => {
expect(Engine.acceptPendingApproval).toHaveBeenCalledTimes(1);
await flushPromises();
expect(mockCaptureSignatureMetrics).toHaveBeenCalledTimes(1);
+ expect(clearSecurityAlertResponseSpy).toHaveBeenCalledTimes(1);
});
it('call required callbacks when reject button is clicked', async () => {
+ const clearSecurityAlertResponseSpy = jest.spyOn(
+ PPOMUtil,
+ 'clearSignatureSecurityAlertResponse',
+ );
const { result } = renderHookWithProvider(() => useConfirmActions(), {
state: personalSignatureConfirmationState,
});
result?.current?.onReject();
expect(Engine.rejectPendingApproval).toHaveBeenCalledTimes(1);
expect(mockCaptureSignatureMetrics).toHaveBeenCalledTimes(1);
+ expect(clearSecurityAlertResponseSpy).toHaveBeenCalledTimes(1);
});
});
diff --git a/app/components/Views/confirmations/hooks/useConfirmActions.ts b/app/components/Views/confirmations/hooks/useConfirmActions.ts
index 01ab81fe5c0..f4af7d4625c 100644
--- a/app/components/Views/confirmations/hooks/useConfirmActions.ts
+++ b/app/components/Views/confirmations/hooks/useConfirmActions.ts
@@ -1,5 +1,6 @@
import { useCallback } from 'react';
+import PPOMUtil from '../../../../lib/ppom/ppom-util';
import { MetaMetricsEvents } from '../../../hooks/useMetrics';
import { isSignatureRequest } from '../utils/confirm';
import useApprovalRequest from './useApprovalRequest';
@@ -24,6 +25,7 @@ export const useConfirmActions = () => {
});
if (signatureRequest) {
captureSignatureMetrics(MetaMetricsEvents.SIGNATURE_APPROVED);
+ PPOMUtil.clearSignatureSecurityAlertResponse();
}
}, [captureSignatureMetrics, onRequestConfirm, signatureRequest]);
@@ -31,6 +33,7 @@ export const useConfirmActions = () => {
onRequestReject();
if (signatureRequest) {
captureSignatureMetrics(MetaMetricsEvents.SIGNATURE_REJECTED);
+ PPOMUtil.clearSignatureSecurityAlertResponse();
}
}, [captureSignatureMetrics, onRequestReject, signatureRequest]);
diff --git a/app/components/Views/confirmations/utils/date.test.ts b/app/components/Views/confirmations/utils/date.test.ts
new file mode 100644
index 00000000000..c117cc72d6a
--- /dev/null
+++ b/app/components/Views/confirmations/utils/date.test.ts
@@ -0,0 +1,17 @@
+import { formatUTCDateFromUnixTimestamp } from './date';
+
+describe('date util', () => {
+ describe('formatUTCDateFromUnixTimestamp', () => {
+ it('formats passed date string', () => {
+ expect(formatUTCDateFromUnixTimestamp(2036528542)).toStrictEqual(
+ '14 July 2034, 22:22',
+ );
+ });
+
+ it('returns empty string if no value is passed', () => {
+ expect(
+ formatUTCDateFromUnixTimestamp(undefined as unknown as number),
+ ).toStrictEqual(undefined);
+ });
+ });
+});
diff --git a/app/components/Views/confirmations/utils/date.ts b/app/components/Views/confirmations/utils/date.ts
new file mode 100644
index 00000000000..57d662a4792
--- /dev/null
+++ b/app/components/Views/confirmations/utils/date.ts
@@ -0,0 +1,23 @@
+import { DateTime } from 'luxon';
+
+/**
+ * @param {number} unixTimestamp - timestamp as seconds since unix epoch
+ * @returns {string} formatted date string e.g. "14 July 2034, 22:22"
+ */
+export const formatUTCDateFromUnixTimestamp = (unixTimestamp: number) => {
+ if (!unixTimestamp) {
+ return unixTimestamp;
+ }
+
+ return DateTime.fromSeconds(unixTimestamp)
+ .toUTC()
+ .toFormat('dd LLLL yyyy, HH:mm');
+};
+
+/**
+ * Date values may include -1 to represent a null value
+ * e.g.
+ * {@see {@link https://eips.ethereum.org/EIPS/eip-2612}}
+ * "The deadline argument can be set to uint(-1) to create Permits that effectively never expire."
+ */
+export const NONE_DATE_VALUE = -1;
diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts
index a8a61fd698f..a8303c37787 100644
--- a/app/core/Analytics/MetaMetrics.events.ts
+++ b/app/core/Analytics/MetaMetrics.events.ts
@@ -356,7 +356,7 @@ enum EVENT_NAME {
RECEIVE_BUTTON_CLICKED = 'Receive Button Clicked',
SWAP_BUTTON_CLICKED = 'Swaps Button Clicked',
SEND_BUTTON_CLICKED = 'Send Button Clicked',
-
+ EARN_BUTTON_CLICKED = 'Earn Button Clicked',
// Edit account name
ACCOUNT_RENAMED = 'Account Renamed',
@@ -820,6 +820,7 @@ const events = {
RECEIVE_BUTTON_CLICKED: generateOpt(EVENT_NAME.RECEIVE_BUTTON_CLICKED),
SWAP_BUTTON_CLICKED: generateOpt(EVENT_NAME.SWAP_BUTTON_CLICKED),
SEND_BUTTON_CLICKED: generateOpt(EVENT_NAME.SEND_BUTTON_CLICKED),
+ EARN_BUTTON_CLICKED: generateOpt(EVENT_NAME.EARN_BUTTON_CLICKED),
NETWORK_SELECTOR_PRESSED: generateOpt(EVENT_NAME.NETWORK_SELECTOR),
// Edit account name
diff --git a/app/core/Analytics/MetricsEventBuilder.test.ts b/app/core/Analytics/MetricsEventBuilder.test.ts
index 38b7bfe75a9..b7e4125b0b2 100644
--- a/app/core/Analytics/MetricsEventBuilder.test.ts
+++ b/app/core/Analytics/MetricsEventBuilder.test.ts
@@ -1,4 +1,4 @@
-import { MetricsEventBuilder } from './MetricsEventBuilder';
+import {MetricsEventBuilder} from './MetricsEventBuilder';
import { IMetaMetricsEvent, JsonMap } from './MetaMetrics.types';
describe('MetricsEventBuilder', () => {
@@ -80,24 +80,24 @@ describe('MetricsEventBuilder', () => {
expect(rebuiltEvent.sensitiveProperties).toEqual(newSensitiveProps);
});
- // test fix for https://github.com/MetaMask/metamask-mobile/issues/12728
- it('adds properties with legacy sensitive properties objects', () => {
+ it('compares events', () => {
const newProps: JsonMap = {
newProp: 'newValue',
- newSensitiveLegacyProp: { value: 'newSensitiveLegacyValue', anonymous: true}
};
- const event = MetricsEventBuilder.createEventBuilder(mockEvent)
- .addProperties(newProps)
+ const event = MetricsEventBuilder.createEventBuilder(mockLegacyEvent)
+ .addSensitiveProperties(newProps)
.build();
- expect(event.properties).toEqual({newProp: 'newValue'});
- expect(event.sensitiveProperties).toEqual({newSensitiveLegacyProp: 'newSensitiveLegacyValue'});
- const rebuiltEvent = MetricsEventBuilder.createEventBuilder(event)
+ const similarEvent = MetricsEventBuilder.createEventBuilder(mockLegacyEvent)
+ .addSensitiveProperties(newProps)
+ .build();
+ expect(similarEvent).toEqual(event);
+
+ const differentEvent = MetricsEventBuilder.createEventBuilder(mockLegacyEvent)
.addProperties(newProps)
.build();
- expect(rebuiltEvent.properties).toEqual({newProp: 'newValue'});
- expect(rebuiltEvent.sensitiveProperties).toEqual({newSensitiveLegacyProp: 'newSensitiveLegacyValue'});
+ expect(differentEvent).not.toEqual(event);
});
it('removes properties', () => {
diff --git a/app/core/Analytics/MetricsEventBuilder.ts b/app/core/Analytics/MetricsEventBuilder.ts
index ccf6093f58d..b4a5d11fc89 100644
--- a/app/core/Analytics/MetricsEventBuilder.ts
+++ b/app/core/Analytics/MetricsEventBuilder.ts
@@ -9,57 +9,29 @@ import {
* the event tracking object to be produced by MetricsEventBuilder
*/
class TrackingEvent implements ITrackingEvent {
- readonly #name: string;
- #properties: JsonMap;
- #sensitiveProperties: JsonMap;
- #saveDataRecording: boolean;
+ readonly name: string;
+ properties: JsonMap;
+ sensitiveProperties: JsonMap;
+ saveDataRecording: boolean;
constructor(event: IMetaMetricsEvent) {
- this.#name = event.category;
- this.#properties = event.properties || {};
- this.#sensitiveProperties = {};
- this.#saveDataRecording = true;
- }
-
- get name(): string {
- return this.#name;
- }
-
- get properties(): JsonMap {
- return this.#properties;
- }
-
- set properties(properties: JsonMap) {
- this.#properties = properties;
- }
-
- get sensitiveProperties(): JsonMap {
- return this.#sensitiveProperties;
- }
-
- set sensitiveProperties(sensitiveProperties: JsonMap) {
- this.#sensitiveProperties = sensitiveProperties;
- }
-
- get saveDataRecording(): boolean {
- return this.#saveDataRecording;
- }
-
- set saveDataRecording(saveDataRecording: boolean) {
- this.#saveDataRecording = saveDataRecording;
+ this.name = event.category;
+ this.properties = event.properties || {};
+ this.sensitiveProperties = {};
+ this.saveDataRecording = true;
}
get isAnonymous(): boolean {
return !!(
- this.#sensitiveProperties && Object.keys(this.#sensitiveProperties).length
+ this.sensitiveProperties && Object.keys(this.sensitiveProperties).length
);
}
get hasProperties(): boolean {
return !!(
- (this.#properties && Object.keys(this.#properties).length) ||
- (this.#sensitiveProperties &&
- Object.keys(this.#sensitiveProperties).length)
+ (this.properties && Object.keys(this.properties).length) ||
+ (this.sensitiveProperties &&
+ Object.keys(this.sensitiveProperties).length)
);
}
}
@@ -85,6 +57,11 @@ class MetricsEventBuilder {
protected constructor(event: IMetaMetricsEvent | ITrackingEvent) {
if (isTrackingEvent(event)) {
+ // Be careful that in case the event is already a ITrackingEvent
+ // we don't want to create a new one so this passes the reference.
+ // Changes applied to the source event will be reflected in the new event.
+ // If at any point you need to clone the ITrackingEvent, it will require to
+ // create a new ITrackingEvent object by copying the values.
this.#trackingEvent = event;
return;
}
diff --git a/app/core/AppStateEventListener.test.ts b/app/core/AppStateEventListener.test.ts
index ede6491cc96..02606e78299 100644
--- a/app/core/AppStateEventListener.test.ts
+++ b/app/core/AppStateEventListener.test.ts
@@ -79,25 +79,30 @@ describe('AppStateEventListener', () => {
mockAppStateListener('active');
jest.advanceTimersByTime(2000);
- expect(mockMetrics.trackEvent).toHaveBeenCalledWith(
- MetricsEventBuilder.createEventBuilder(MetaMetricsEvents.APP_OPENED)
- .addSensitiveProperties({
- attributionId: 'test123',
- utm_source: 'source',
- utm_medium: 'medium',
- utm_campaign: 'campaign',
- })
- .build(),
- );
+ const expectedEvent = MetricsEventBuilder.createEventBuilder(MetaMetricsEvents.APP_OPENED)
+ .addProperties({
+ attributionId: 'test123',
+ utm_source: 'source',
+ utm_medium: 'medium',
+ utm_campaign: 'campaign',
+ })
+ .build();
+
+ expect(mockMetrics.trackEvent).toHaveBeenCalledWith(expectedEvent);
});
- it('does not track event when processAttribution returns undefined', () => {
+ it('tracks event when app becomes active without attribution data', () => {
+ jest
+ .spyOn(ReduxService, 'store', 'get')
+ .mockReturnValue({} as unknown as ReduxStore);
(processAttribution as jest.Mock).mockReturnValue(undefined);
mockAppStateListener('active');
jest.advanceTimersByTime(2000);
- expect(mockMetrics.trackEvent).not.toHaveBeenCalled();
+ expect(mockMetrics.trackEvent).toHaveBeenCalledWith(
+ MetricsEventBuilder.createEventBuilder(MetaMetricsEvents.APP_OPENED).build()
+ );
});
it('handles errors gracefully', () => {
diff --git a/app/core/AppStateEventListener.ts b/app/core/AppStateEventListener.ts
index 2376c337494..f6be9afc39c 100644
--- a/app/core/AppStateEventListener.ts
+++ b/app/core/AppStateEventListener.ts
@@ -48,18 +48,16 @@ export class AppStateEventListener {
currentDeeplink: this.currentDeeplink,
store: ReduxService.store,
});
+ const appOpenedEventBuilder = MetricsEventBuilder.createEventBuilder(MetaMetricsEvents.APP_OPENED);
if (attribution) {
const { attributionId, utm, ...utmParams } = attribution;
DevLogger.log(
`AppStateManager:: processAppStateChange:: sending event 'APP_OPENED' attributionId=${attribution.attributionId} utm=${attribution.utm}`,
utmParams,
);
- MetaMetrics.getInstance().trackEvent(
- MetricsEventBuilder.createEventBuilder(MetaMetricsEvents.APP_OPENED)
- .addSensitiveProperties({ attributionId, ...utmParams })
- .build(),
- );
+ appOpenedEventBuilder.addProperties({ attributionId, ...utmParams });
}
+ MetaMetrics.getInstance().trackEvent(appOpenedEventBuilder.build());
} catch (error) {
Logger.error(
error as Error,
diff --git a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts
index b7eed8fd925..9c8020392f1 100644
--- a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts
+++ b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts
@@ -1,15 +1,14 @@
+import { Platform } from 'react-native';
import { ACTIONS, PREFIXES } from '../../../constants/deeplinks';
+import Routes from '../../../constants/navigation/Routes';
+import Device from '../../../util/device';
import AppConstants from '../../AppConstants';
-import { Minimizer } from '../../NativeModules';
+import handleDeeplink from '../../SDKConnect/handlers/handleDeeplink';
import SDKConnect from '../../SDKConnect/SDKConnect';
import WC2Manager from '../../WalletConnect/WalletConnectV2';
import DeeplinkManager from '../DeeplinkManager';
import extractURLParams from './extractURLParams';
import handleMetaMaskDeeplink from './handleMetaMaskDeeplink';
-import handleDeeplink from '../../SDKConnect/handlers/handleDeeplink';
-import Device from '../../../util/device';
-import { Platform } from 'react-native';
-import Routes from '../../../constants/navigation/Routes';
jest.mock('../../../core/AppConstants');
jest.mock('../../../core/SDKConnect/handlers/handleDeeplink');
@@ -269,49 +268,7 @@ describe('handleMetaMaskProtocol', () => {
url = `${PREFIXES.METAMASK}${ACTIONS.CONNECT}`;
});
- it('should call Minimizer.goBack if params.redirect is truthy on android', () => {
- params.redirect = 'true';
- // Mock Device.isIos() to return true
- jest.spyOn(Device, 'isIos').mockReturnValue(false);
-
- // Set Platform.Version to '16' to ensure it's less than 17
- Object.defineProperty(Platform, 'Version', { get: () => '16' });
-
- handleMetaMaskDeeplink({
- instance,
- handled,
- params,
- origin: AppConstants.DEEPLINKS.ORIGIN_DEEPLINK,
- wcURL,
- url,
- });
-
- expect(handled).toHaveBeenCalled();
- expect(Minimizer.goBack).toHaveBeenCalled();
- });
-
- it('should call Minimizer.goBack if params.redirect is truthy on ios <17', () => {
- params.redirect = 'true';
- // Mock Device.isIos() to return true
- jest.spyOn(Device, 'isIos').mockReturnValue(true);
-
- // Set Platform.Version to '16' to ensure it's less than 17
- Object.defineProperty(Platform, 'Version', { get: () => '16' });
-
- handleMetaMaskDeeplink({
- instance,
- handled,
- params,
- origin: AppConstants.DEEPLINKS.ORIGIN_DEEPLINK,
- wcURL,
- url,
- });
-
- expect(handled).toHaveBeenCalled();
- expect(Minimizer.goBack).toHaveBeenCalled();
- });
-
- it('should displays RETURN_TO_DAPP_MODAL if params.redirect is truthy on ios >17', () => {
+ it('should displays RETURN_TO_DAPP_MODAL', () => {
params.redirect = 'true';
// Mock Device.isIos() to return true
jest.spyOn(Device, 'isIos').mockReturnValue(true);
@@ -332,7 +289,6 @@ describe('handleMetaMaskProtocol', () => {
expect(mockNavigate).toHaveBeenCalledWith(Routes.MODAL.ROOT_MODAL_FLOW, {
screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
});
- expect(Minimizer.goBack).not.toHaveBeenCalled();
});
diff --git a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts
index 6cb1fbabfee..544186f29d6 100644
--- a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts
+++ b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts
@@ -1,18 +1,15 @@
import { OriginatorInfo } from '@metamask/sdk-communication-layer';
import { ACTIONS, PREFIXES } from '../../../constants/deeplinks';
+import Routes from '../../../constants/navigation/Routes';
import Logger from '../../../util/Logger';
-import { Minimizer } from '../../NativeModules';
+import AppConstants from '../../AppConstants';
import SDKConnect from '../../SDKConnect/SDKConnect';
import handleDeeplink from '../../SDKConnect/handlers/handleDeeplink';
import DevLogger from '../../SDKConnect/utils/DevLogger';
import WC2Manager from '../../WalletConnect/WalletConnectV2';
import DeeplinkManager from '../DeeplinkManager';
-import extractURLParams from './extractURLParams';
import parseOriginatorInfo from '../parseOriginatorInfo';
-import { Platform } from 'react-native';
-import Device from '../../../util/device';
-import Routes from '../../../constants/navigation/Routes';
-import AppConstants from '../../AppConstants';
+import extractURLParams from './extractURLParams';
export function handleMetaMaskDeeplink({
instance,
handled,
@@ -43,13 +40,9 @@ export function handleMetaMaskDeeplink({
if (url.startsWith(`${PREFIXES.METAMASK}${ACTIONS.CONNECT}`)) {
if (params.redirect && origin === AppConstants.DEEPLINKS.ORIGIN_DEEPLINK) {
- if (Device.isIos() && parseInt(Platform.Version as string) >= 17) {
- SDKConnect.getInstance().state.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
- screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
- });
- } else {
- Minimizer.goBack();
- }
+ SDKConnect.getInstance().state.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
+ });
} else if (params.channelId) {
// differentiate between deeplink callback and socket connection
if (params.comm === 'deeplinking') {
diff --git a/app/core/DeeplinkManager/ParseManager/handleUniversalLink.test.ts b/app/core/DeeplinkManager/ParseManager/handleUniversalLink.test.ts
index e0764f00e59..d26c869b286 100644
--- a/app/core/DeeplinkManager/ParseManager/handleUniversalLink.test.ts
+++ b/app/core/DeeplinkManager/ParseManager/handleUniversalLink.test.ts
@@ -2,7 +2,6 @@ import { Platform } from 'react-native';
import { ACTIONS } from '../../../constants/deeplinks';
import Device from '../../../util/device';
import AppConstants from '../../AppConstants';
-import { Minimizer } from '../../NativeModules';
import SDKConnect from '../../SDKConnect/SDKConnect';
import handleDeeplink from '../../SDKConnect/handlers/handleDeeplink';
import DevLogger from '../../SDKConnect/utils/DevLogger';
@@ -122,63 +121,7 @@ describe('handleUniversalLinks', () => {
});
describe('ACTIONS.CONNECT', () => {
- it('should call Minimizer.goBack if params.redirect is truthy on android', () => {
- params.redirect = 'true';
- // Mock Device.isIos() to return true
- jest.spyOn(Device, 'isIos').mockReturnValue(false);
-
- // Set Platform.Version to '16' to ensure it's less than 17
- Object.defineProperty(Platform, 'Version', { get: () => '16' });
-
- urlObj = {
- hostname: AppConstants.MM_UNIVERSAL_LINK_HOST,
- pathname: `/${ACTIONS.CONNECT}/additional/path`,
- } as ReturnType['urlObj'];
-
- handleUniversalLink({
- instance,
- handled,
- urlObj,
- params,
- browserCallBack: mockBrowserCallBack,
- origin: AppConstants.DEEPLINKS.ORIGIN_DEEPLINK,
- wcURL,
- url,
- });
-
- expect(handled).toHaveBeenCalled();
- expect(Minimizer.goBack).toHaveBeenCalled();
- });
-
- it('should call Minimizer.goBack if params.redirect is truthy on ios <17', () => {
- params.redirect = 'true';
- // Mock Device.isIos() to return true
- jest.spyOn(Device, 'isIos').mockReturnValue(false);
-
- // Set Platform.Version to '16' to ensure it's less than 17
- Object.defineProperty(Platform, 'Version', { get: () => '16' });
-
- urlObj = {
- hostname: AppConstants.MM_UNIVERSAL_LINK_HOST,
- pathname: `/${ACTIONS.CONNECT}/additional/path`,
- } as ReturnType['urlObj'];
-
- handleUniversalLink({
- instance,
- handled,
- urlObj,
- params,
- browserCallBack: mockBrowserCallBack,
- origin: AppConstants.DEEPLINKS.ORIGIN_DEEPLINK,
- wcURL,
- url,
- });
-
- expect(handled).toHaveBeenCalled();
- expect(Minimizer.goBack).toHaveBeenCalled();
- });
-
- it('should displays RETURN_TO_DAPP_MODAL if params.redirect is truthy on ios >17', () => {
+ it('should displays RETURN_TO_DAPP_MODAL', () => {
params.redirect = 'true';
// Mock Device.isIos() to return true
jest.spyOn(Device, 'isIos').mockReturnValue(true);
@@ -215,30 +158,6 @@ describe('handleUniversalLinks', () => {
expect(mockNavigate).toHaveBeenCalledWith(Routes.MODAL.ROOT_MODAL_FLOW, {
screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
});
- expect(Minimizer.goBack).not.toHaveBeenCalled();
- });
-
- it('should NOT call Minimizer.goBack if params.redirect is falsy', () => {
- params.redirect = '';
-
- urlObj = {
- hostname: AppConstants.MM_UNIVERSAL_LINK_HOST,
- pathname: `/${ACTIONS.CONNECT}/additional/path`,
- } as ReturnType['urlObj'];
-
- handleUniversalLink({
- instance,
- handled,
- urlObj,
- params,
- browserCallBack: mockBrowserCallBack,
- origin,
- wcURL,
- url,
- });
-
- expect(handled).toHaveBeenCalled();
- expect(Minimizer.goBack).not.toHaveBeenCalled();
});
});
diff --git a/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts b/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts
index b447dd64f37..4ac42b61e7c 100644
--- a/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts
+++ b/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts
@@ -1,18 +1,15 @@
+import { OriginatorInfo } from '@metamask/sdk-communication-layer';
import { ACTIONS, PREFIXES, PROTOCOLS } from '../../../constants/deeplinks';
+import Routes from '../../../constants/navigation/Routes';
+import Logger from '../../../util/Logger';
import AppConstants from '../../AppConstants';
-import { Minimizer } from '../../NativeModules';
import SDKConnect from '../../SDKConnect/SDKConnect';
import handleDeeplink from '../../SDKConnect/handlers/handleDeeplink';
import DevLogger from '../../SDKConnect/utils/DevLogger';
import WC2Manager from '../../WalletConnect/WalletConnectV2';
-import Logger from '../../../util/Logger';
import DeeplinkManager from '../DeeplinkManager';
-import extractURLParams from './extractURLParams';
-import { OriginatorInfo } from '@metamask/sdk-communication-layer';
import parseOriginatorInfo from '../parseOriginatorInfo';
-import Device from '../../../util/device';
-import { Platform } from 'react-native';
-import Routes from '../../../constants/navigation/Routes';
+import extractURLParams from './extractURLParams';
function handleUniversalLink({
instance,
@@ -57,13 +54,9 @@ function handleUniversalLink({
if (action === ACTIONS.CONNECT) {
if (params.redirect && origin === AppConstants.DEEPLINKS.ORIGIN_DEEPLINK) {
- if (Device.isIos() && parseInt(Platform.Version as string) >= 17) {
- SDKConnect.getInstance().state.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
- screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
- });
- } else {
- Minimizer.goBack();
- }
+ SDKConnect.getInstance().state.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
+ });
} else if (params.channelId) {
const protocolVersion = parseInt(params.v ?? '1', 10);
diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts
index 6285908493f..cae00ab9c54 100644
--- a/app/core/EngineService/EngineService.ts
+++ b/app/core/EngineService/EngineService.ts
@@ -1,5 +1,5 @@
-import Engine from '../Engine';
-import AppConstants from '../AppConstants';
+import UntypedEngine from '../Engine';
+import { Engine as TypedEngine } from '../Engine/Engine';
import { getVaultFromBackup } from '../BackupVault';
import Logger from '../../util/Logger';
import {
@@ -9,6 +9,7 @@ import {
import { getTraceTags } from '../../util/sentry/tags';
import { trace, endTrace, TraceName, TraceOperation } from '../../util/trace';
import getUIStartupSpan from '../Performance/UIStartup';
+import { BACKGROUND_STATE_CHANGE_EVENT_NAMES } from '../Engine/constants';
import ReduxService from '../redux';
import NavigationService from '../NavigationService';
import Routes from '../../constants/navigation/Routes';
@@ -52,7 +53,8 @@ export class EngineService {
parentContext: getUIStartupSpan(),
tags: getTraceTags(reduxState),
});
- const state = reduxState?.engine?.backgroundState || {};
+ const state = reduxState?.engine?.backgroundState ?? {};
+ const Engine = UntypedEngine;
try {
Logger.log(`${LOG_TAG}: Initializing Engine:`, {
hasState: Object.keys(state).length > 0,
@@ -60,7 +62,8 @@ export class EngineService {
const metaMetricsId = await MetaMetrics.getInstance().getMetaMetricsId();
Engine.init(state, null, metaMetricsId);
- this.updateControllers(Engine);
+ // `Engine.init()` call mutates `typeof UntypedEngine` to `TypedEngine`
+ this.updateControllers(Engine as unknown as TypedEngine);
} catch (error) {
Logger.error(
error as Error,
@@ -74,9 +77,7 @@ export class EngineService {
endTrace({ name: TraceName.EngineInitialization });
};
- // TODO: Replace "any" with type
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- private updateControllers = (engine: any) => {
+ private updateControllers = (engine: TypedEngine) => {
if (!engine.context) {
Logger.error(
new Error(
@@ -86,125 +87,6 @@ export class EngineService {
return;
}
- const controllers = [
- {
- name: 'AddressBookController',
- key: `${engine.context.AddressBookController.name}:stateChange`,
- },
- { name: 'NftController', key: 'NftController:stateChange' },
- {
- name: 'TokensController',
- key: `${engine.context.TokensController.name}:stateChange`,
- },
- {
- name: 'KeyringController',
- key: `${engine.context.KeyringController.name}:stateChange`,
- },
- {
- name: 'AccountTrackerController',
- key: 'AccountTrackerController:stateChange',
- },
- {
- name: 'NetworkController',
- key: AppConstants.NETWORK_STATE_CHANGE_EVENT,
- },
- {
- name: 'PhishingController',
- key: `${engine.context.PhishingController.name}:stateChange`,
- },
- {
- name: 'PreferencesController',
- key: `${engine.context.PreferencesController.name}:stateChange`,
- },
- {
- name: 'RemoteFeatureFlagController',
- key: `${engine.context.RemoteFeatureFlagController.name}:stateChange`,
- },
- {
- name: 'SelectedNetworkController',
- key: `${engine.context.SelectedNetworkController.name}:stateChange`,
- },
- {
- name: 'TokenBalancesController',
- key: `${engine.context.TokenBalancesController.name}:stateChange`,
- },
- { name: 'TokenRatesController', key: 'TokenRatesController:stateChange' },
- {
- name: 'TransactionController',
- key: `${engine.context.TransactionController.name}:stateChange`,
- },
- {
- name: 'SmartTransactionsController',
- key: `${engine.context.SmartTransactionsController.name}:stateChange`,
- },
- {
- name: 'SwapsController',
- key: `${engine.context.SwapsController.name}:stateChange`,
- },
- {
- name: 'TokenListController',
- key: `${engine.context.TokenListController.name}:stateChange`,
- },
- {
- name: 'CurrencyRateController',
- key: `${engine.context.CurrencyRateController.name}:stateChange`,
- },
- {
- name: 'GasFeeController',
- key: `${engine.context.GasFeeController.name}:stateChange`,
- },
- {
- name: 'ApprovalController',
- key: `${engine.context.ApprovalController.name}:stateChange`,
- },
- ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
- {
- name: 'SnapController',
- key: `${engine.context.SnapController.name}:stateChange`,
- },
- {
- name: 'SubjectMetadataController',
- key: `${engine.context.SubjectMetadataController.name}:stateChange`,
- },
- {
- name: 'AuthenticationController',
- key: 'AuthenticationController:stateChange',
- },
- {
- name: 'UserStorageController',
- key: 'UserStorageController:stateChange',
- },
- {
- name: 'NotificationServicesController',
- key: 'NotificationServicesController:stateChange',
- },
- {
- name: 'NotificationServicesPushController',
- key: 'NotificationServicesPushController:stateChange',
- },
- ///: END:ONLY_INCLUDE_IF
- {
- name: 'PermissionController',
- key: `${engine.context.PermissionController.name}:stateChange`,
- },
- {
- name: 'LoggingController',
- key: `${engine.context.LoggingController.name}:stateChange`,
- },
- {
- name: 'AccountsController',
- key: `${engine.context.AccountsController.name}:stateChange`,
- },
- {
- name: 'PPOMController',
- key: `${engine.context.PPOMController.name}:stateChange`,
- },
- {
- name: 'SignatureController',
- key: `${engine.context.SignatureController.name}:stateChange`,
- },
- ];
-
engine.controllerMessenger.subscribeOnceIf(
'ComposableController:stateChange',
() => {
@@ -217,18 +99,20 @@ export class EngineService {
() => !this.engineInitialized,
);
- controllers.forEach((controller) => {
- const { name, key } = controller;
- const update_bg_state_cb = () => {
- if (!engine.context.KeyringController.metadata.vault) {
- Logger.log('keyringController vault missing for UPDATE_BG_STATE_KEY');
- }
- ReduxService.store.dispatch({
- type: UPDATE_BG_STATE_KEY,
- payload: { key: name },
- });
- };
- engine.controllerMessenger.subscribe(key, update_bg_state_cb);
+ const update_bg_state_cb = (controllerName: string) => {
+ if (!engine.context.KeyringController.metadata.vault) {
+ Logger.log('keyringController vault missing for UPDATE_BG_STATE_KEY');
+ }
+ ReduxService.store.dispatch({
+ type: UPDATE_BG_STATE_KEY,
+ payload: { key: controllerName },
+ });
+ };
+
+ BACKGROUND_STATE_CHANGE_EVENT_NAMES.forEach((eventName) => {
+ engine.controllerMessenger.subscribe(eventName, () =>
+ update_bg_state_cb(eventName.split(':')[0]),
+ );
});
};
@@ -244,7 +128,8 @@ export class EngineService {
async initializeVaultFromBackup(): Promise {
const keyringState = await getVaultFromBackup();
const reduxState = ReduxService.store.getState();
- const state = reduxState?.engine?.backgroundState || {};
+ const state = reduxState?.engine?.backgroundState ?? {};
+ const Engine = UntypedEngine;
// This ensures we create an entirely new engine
await Engine.destroyEngine();
this.engineInitialized = false;
diff --git a/app/core/Multichain/constants.ts b/app/core/Multichain/constants.ts
index 82cdb75b368..132c0ca3530 100644
--- a/app/core/Multichain/constants.ts
+++ b/app/core/Multichain/constants.ts
@@ -1,4 +1,8 @@
export enum MultichainNetworks {
BITCOIN = 'bip122:000000000019d6689c085ae165831e93',
BITCOIN_TESTNET = 'bip122:000000000933ea01ad0ee984209779ba',
+
+ SOLANA = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
+ SOLANA_DEVNET = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',
+ SOLANA_TESTNET = 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z',
}
diff --git a/app/core/Multichain/test/utils.test.ts b/app/core/Multichain/test/utils.test.ts
index 0553a6ae20d..51d3bb96418 100644
--- a/app/core/Multichain/test/utils.test.ts
+++ b/app/core/Multichain/test/utils.test.ts
@@ -5,6 +5,9 @@ import {
BtcMethod,
EthScopes,
BtcScopes,
+ SolScopes,
+ SolAccountType,
+ SolMethod,
} from '@metamask/keyring-api';
import { InternalAccount } from '@metamask/keyring-internal-api';
import {
@@ -13,6 +16,7 @@ import {
isBtcMainnetAddress,
isBtcTestnetAddress,
getFormattedAddressFromInternalAccount,
+ isSolanaAccount,
} from '../utils';
import { KeyringTypes } from '@metamask/keyring-controller';
import { toChecksumHexAddress } from '@metamask/controller-utils';
@@ -25,7 +29,7 @@ const MOCK_BTC_MAINNET_ADDRESS_2 = '1P5ZEDWTKTFGxQjZphgWPQUpe554WKDfHQ';
const MOCK_BTC_TESTNET_ADDRESS = 'tb1q63st8zfndjh00gf9hmhsdg7l8umuxudrj4lucp';
const MOCK_ETH_ADDRESS = '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272';
-const SOL_ADDRESSES = '7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV';
+const SOL_ADDRESS = '7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV';
const mockEthEOAAccount: InternalAccount = {
address: MOCK_ETH_ADDRESS,
@@ -79,7 +83,12 @@ const mockBTCAccount: InternalAccount = {
name: 'Bitcoin Account',
importTime: 1684232000456,
keyring: {
- type: KeyringTypes.hd,
+ type: KeyringTypes.snap,
+ },
+ snap: {
+ id: 'npm:"@metamask/bitcoin-wallet-snap',
+ name: 'Bitcoin Wallet Snap',
+ enabled: true,
},
},
options: {},
@@ -87,6 +96,27 @@ const mockBTCAccount: InternalAccount = {
type: BtcAccountType.P2wpkh,
};
+const mockSolAccount: InternalAccount = {
+ address: SOL_ADDRESS,
+ id: '1',
+ type: SolAccountType.DataAccount,
+ methods: [SolMethod.SendAndConfirmTransaction],
+ options: {},
+ metadata: {
+ name: 'Solana Account',
+ importTime: 1684232000456,
+ keyring: {
+ type: KeyringTypes.snap,
+ },
+ snap: {
+ id: 'npm:"@metamask/solana-wallet-snap',
+ name: 'Solana Wallet Snap',
+ enabled: true,
+ },
+ },
+ scopes: [SolScopes.Mainnet, SolScopes.Testnet, SolScopes.Devnet],
+};
+
describe('MultiChain utils', () => {
describe('isEthAccount', () => {
it('returns true for EOA accounts', () => {
@@ -129,7 +159,7 @@ describe('MultiChain utils', () => {
expect(isBtcMainnetAddress(MOCK_ETH_ADDRESS)).toBe(false);
});
it('returns false for SOL addresses', () => {
- expect(isBtcMainnetAddress(SOL_ADDRESSES)).toBe(false);
+ expect(isBtcMainnetAddress(SOL_ADDRESS)).toBe(false);
});
});
@@ -147,9 +177,21 @@ describe('MultiChain utils', () => {
expect(isBtcTestnetAddress(MOCK_ETH_ADDRESS)).toBe(false);
});
it('returns false for SOL addresses', () => {
- expect(isBtcTestnetAddress(SOL_ADDRESSES)).toBe(false);
+ expect(isBtcTestnetAddress(SOL_ADDRESS)).toBe(false);
});
});
+
+ describe('isSolanaAccount', () => {
+ it('returns true for Solana accounts', () => {
+ expect(isSolanaAccount(mockSolAccount)).toBe(true);
+ });
+
+ it('returns false for non-Solana accounts', () => {
+ expect(isSolanaAccount(mockEthEOAAccount)).toBe(false);
+ expect(isSolanaAccount(mockBTCAccount)).toBe(false);
+ });
+ });
+
describe('getFormattedAddressFromInternalAccount', () => {
it('returns checksummed address for ETH EOA accounts', () => {
const formatted =
diff --git a/app/core/Multichain/utils.ts b/app/core/Multichain/utils.ts
index b853c207f97..199391044c5 100644
--- a/app/core/Multichain/utils.ts
+++ b/app/core/Multichain/utils.ts
@@ -8,6 +8,7 @@ import {
} from '@metamask/keyring-api';
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import { validate, Network } from 'bitcoin-address-validation';
+import { isAddress as isSolanaAddress } from '@solana/addresses';
///: END:ONLY_INCLUDE_IF
/**
@@ -77,4 +78,16 @@ export function isBtcMainnetAddress(address: string): boolean {
export function isBtcTestnetAddress(address: string): boolean {
return validate(address, Network.testnet);
}
+
+/**
+ * Returns whether an address is a valid Solana address, specifically an account's.
+ * Derived addresses (like Program's) will return false.
+ * See: https://stackoverflow.com/questions/71200948/how-can-i-validate-a-solana-wallet-address-with-web3js
+ *
+ * @param address - The address to check.
+ * @returns `true` if the address is a valid Solana address, `false` otherwise.
+ */
+export function isSolanaAccount(account: InternalAccount): boolean {
+ return isSolanaAddress(account.address);
+}
///: END:ONLY_INCLUDE_IF
diff --git a/app/core/NotificationManager.js b/app/core/NotificationManager.js
index ee50c425e43..9588923a4d0 100644
--- a/app/core/NotificationManager.js
+++ b/app/core/NotificationManager.js
@@ -234,14 +234,16 @@ class NotificationManager {
// Detect assets and tokens for ERC20 txs
// Detect assets for ERC721 txs
// right after a transaction was confirmed
- const pollPromises = [AccountTrackerController.refresh()];
+ const pollPromises = [
+ AccountTrackerController.refresh(),
+ TokenBalancesController.updateBalancesByChainId({
+ chainId: transactionMeta.chainId,
+ }),
+ ];
switch (originalTransaction.assetType) {
case 'ERC20': {
pollPromises.push(
...[
- TokenBalancesController.updateBalancesByChainId({
- chainId: transactionMeta.chainId,
- }),
TokenDetectionController.detectTokens({
chainIds: [transactionMeta.chainId],
}),
@@ -430,10 +432,7 @@ class NotificationManager {
*/
gotIncomingTransaction = async (incomingTransactions) => {
try {
- const {
- AccountTrackerController,
- AccountsController,
- } = Engine.context;
+ const { AccountTrackerController, AccountsController } = Engine.context;
const selectedInternalAccount = AccountsController.getSelectedAccount();
@@ -446,13 +445,14 @@ class NotificationManager {
// If a TX has been confirmed more than 10 min ago, it's considered old
const oldestTimeAllowed = Date.now() - 1000 * 60 * 10;
- const filteredTransactions = incomingTransactions.reverse()
+ const filteredTransactions = incomingTransactions
+ .reverse()
.filter(
(tx) =>
safeToChecksumAddress(tx.txParams?.to) ===
selectedInternalAccountChecksummedAddress &&
safeToChecksumAddress(tx.txParams?.from) !==
- selectedInternalAccountChecksummedAddress &&
+ selectedInternalAccountChecksummedAddress &&
tx.status === TransactionStatus.confirmed &&
tx.time > oldestTimeAllowed,
);
@@ -462,7 +462,9 @@ class NotificationManager {
}
const nonce = hexToBN(filteredTransactions[0].txParams.nonce).toString();
- const amount = renderFromWei(hexToBN(filteredTransactions[0].txParams.value));
+ const amount = renderFromWei(
+ hexToBN(filteredTransactions[0].txParams.value),
+ );
const id = filteredTransactions[0]?.id;
this._showNotification({
@@ -480,7 +482,11 @@ class NotificationManager {
// Update balance upon detecting a new incoming transaction
AccountTrackerController.refresh();
} catch (error) {
- Logger.log('Notifications', 'Error while processing incoming transaction', error);
+ Logger.log(
+ 'Notifications',
+ 'Error while processing incoming transaction',
+ error,
+ );
}
};
}
diff --git a/app/core/SDKConnect/AndroidSDK/AndroidService.ts b/app/core/SDKConnect/AndroidSDK/AndroidService.ts
index d713a2397dc..727ab9df04a 100644
--- a/app/core/SDKConnect/AndroidSDK/AndroidService.ts
+++ b/app/core/SDKConnect/AndroidSDK/AndroidService.ts
@@ -2,7 +2,6 @@ import { NetworkController } from '@metamask/network-controller';
import { EventEmitter2 } from 'eventemitter2';
import { NativeModules } from 'react-native';
import Engine from '../../Engine';
-import { Minimizer } from '../../NativeModules';
import { RPCQueueManager } from '../RPCQueueManager';
import {
@@ -36,6 +35,7 @@ import { DappClient, DappConnections } from './dapp-sdk-types';
import getDefaultBridgeParams from './getDefaultBridgeParams';
import { AccountsController } from '@metamask/accounts-controller';
import { toChecksumHexAddress } from '@metamask/controller-utils';
+import Routes from '../../../constants/navigation/Routes';
export default class AndroidService extends EventEmitter2 {
public communicationClient = NativeModules.CommunicationClient;
@@ -233,7 +233,9 @@ export default class AndroidService extends EventEmitter2 {
`AndroidService::clients_connected error failed sending jsonrpc error to client`,
);
});
- Minimizer.goBack();
+ SDKConnect.getInstance().state.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
+ });
return;
}
diff --git a/app/core/SDKConnect/AndroidSDK/AndroidService/sendMessage.ts b/app/core/SDKConnect/AndroidSDK/AndroidService/sendMessage.ts
index f9b6c6642f6..d46b4b22e6b 100644
--- a/app/core/SDKConnect/AndroidSDK/AndroidService/sendMessage.ts
+++ b/app/core/SDKConnect/AndroidSDK/AndroidService/sendMessage.ts
@@ -1,13 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import Engine from '../../../Engine';
-import { Minimizer } from '../../../NativeModules';
+import { AccountsController } from '@metamask/accounts-controller';
import Logger from '../../../../util/Logger';
-import { wait } from '../../utils/wait.util';
-import AndroidService from '../AndroidService';
+import Engine from '../../../Engine';
import { METHODS_TO_DELAY, RPC_METHODS } from '../../SDKConnectConstants';
import handleBatchRpcResponse from '../../handlers/handleBatchRpcResponse';
import DevLogger from '../../utils/DevLogger';
-import { AccountsController } from '@metamask/accounts-controller';
+import { wait } from '../../utils/wait.util';
+import AndroidService from '../AndroidService';
async function sendMessage(
instance: AndroidService,
@@ -120,7 +119,6 @@ async function sendMessage(
}
DevLogger.log(`AndroidService::sendMessage empty --- goBack()`);
- Minimizer.goBack();
} catch (error) {
Logger.log(error, `AndroidService:: error waiting for empty rpc queue`);
}
diff --git a/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts b/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts
index 7f7e6a19625..3dba91c3744 100644
--- a/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts
+++ b/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts
@@ -1,14 +1,12 @@
import { OriginatorInfo } from '@metamask/sdk-communication-layer';
-import { Platform } from 'react-native';
import Routes from '../../../../constants/navigation/Routes';
+import AppConstants from '../../../../core/AppConstants';
import Logger from '../../../../util/Logger';
-import Device from '../../../../util/device';
import Engine from '../../../Engine';
import SDKConnect, { approveHostProps } from '../../SDKConnect';
import handleConnectionReady from '../../handlers/handleConnectionReady';
import DevLogger from '../../utils/DevLogger';
import { Connection } from '../Connection';
-import AppConstants from '../../../../core/AppConstants';
function handleClientsReady({
instance,
@@ -48,12 +46,9 @@ function handleClientsReady({
instance.trigger === 'deeplink' &&
instance.origin !== AppConstants.DEEPLINKS.ORIGIN_QR_CODE
) {
- // Check for iOS 17 and above to use a custom modal, as Minimizer.goBack() is incompatible with these versions
- if (Device.isIos() && parseInt(Platform.Version as string) >= 17) {
- instance.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
- screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
- });
- }
+ instance.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
+ });
}
},
disapprove,
diff --git a/app/core/SDKConnect/ConnectionManagement/connectToChannel.ts b/app/core/SDKConnect/ConnectionManagement/connectToChannel.ts
index ffc3fb6c0db..4ccf8ff18aa 100644
--- a/app/core/SDKConnect/ConnectionManagement/connectToChannel.ts
+++ b/app/core/SDKConnect/ConnectionManagement/connectToChannel.ts
@@ -1,21 +1,18 @@
import { MessageType, SendAnalytics, TrackingEvents } from '@metamask/sdk-communication-layer';
-import { Platform } from 'react-native';
import { resetConnections } from '../../../../app/actions/sdk';
import { store } from '../../../../app/store';
import Routes from '../../../constants/navigation/Routes';
import { selectChainId } from '../../../selectors/networkController';
-import Device from '../../../util/device';
+import Logger from '../../../util/Logger';
+import AppConstants from '../../AppConstants';
import Engine from '../../Engine';
-import { Minimizer } from '../../NativeModules';
import { getPermittedAccounts } from '../../Permissions';
import { Connection, ConnectionProps } from '../Connection';
import checkPermissions from '../handlers/checkPermissions';
import { DEFAULT_SESSION_TIMEOUT_MS } from '../SDKConnectConstants';
import DevLogger from '../utils/DevLogger';
-import { SDKConnect } from './../SDKConnect';
import { wait, waitForCondition } from '../utils/wait.util';
-import Logger from '../../../util/Logger';
-import AppConstants from '../../AppConstants';
+import { SDKConnect } from './../SDKConnect';
import packageJSON from '../../../../package.json';
const { version: walletVersion } = packageJSON;
@@ -166,15 +163,9 @@ async function connectToChannel({
// cleanup connection
await wait(100); // Add delay for connect modal to be fully closed
await instance.updateSDKLoadingState({ channelId: id, loading: false });
- // Check for iOS 17 and above to use a custom modal, as Minimizer.goBack() is incompatible with these versions
- if (Device.isIos() && parseInt(Platform.Version as string) >= 17) {
- connected.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
- screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
- });
- } else {
- DevLogger.log(`[handleSendMessage] goBack()`);
- await Minimizer.goBack();
- }
+ connected.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
+ });
return;
}
}
@@ -239,14 +230,10 @@ async function connectToChannel({
connected.trigger === AppConstants.DEEPLINKS.ORIGIN_DEEPLINK &&
connected.origin === AppConstants.DEEPLINKS.ORIGIN_DEEPLINK
) {
- if (Device.isIos() && parseInt(Platform.Version as string) >= 17) {
- DevLogger.log(`[handleSendMessage] display RETURN_TO_DAPP_MODAL`);
- connected.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
- screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
- });
- } else {
- await Minimizer.goBack();
- }
+ DevLogger.log(`[handleSendMessage] display RETURN_TO_DAPP_MODAL`);
+ connected.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
+ });
}
}
} catch (error) {
diff --git a/app/core/SDKConnect/handlers/handleSendMessage.test.ts b/app/core/SDKConnect/handlers/handleSendMessage.test.ts
index 07406a660a9..ee01e7cc5fb 100644
--- a/app/core/SDKConnect/handlers/handleSendMessage.test.ts
+++ b/app/core/SDKConnect/handlers/handleSendMessage.test.ts
@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import Device from '../../../util/device';
-import { Minimizer } from '../../NativeModules';
import { Connection } from '../Connection';
import { RPC_METHODS } from '../SDKConnectConstants';
import DevLogger from '../utils/DevLogger';
@@ -27,7 +26,6 @@ describe('handleSendMessage', () => {
const mockDevLogger = DevLogger.log as jest.MockedFunction<
typeof DevLogger.log
>;
- const mockMinimizer = Minimizer as jest.MockedFunction;
const mockSendMessage = jest.fn();
const mockSetLoading = jest.fn();
@@ -203,20 +201,6 @@ describe('handleSendMessage', () => {
mockCanRedirect.mockReturnValue(true);
mockConnection.trigger = 'deeplink';
});
- it('should handle deeplink trigger', async () => {
- mockConnection.trigger = 'deeplink';
-
- await handleSendMessage({
- msg: {
- data: {
- id: 1,
- },
- },
- connection: mockConnection,
- });
-
- expect(mockMinimizer.goBack).toHaveBeenCalled();
- });
it('should wait for specific methods', async () => {
mockRpcQueueManagerGetId.mockReturnValue(RPC_METHODS.METAMASK_BATCH);
@@ -254,39 +238,6 @@ describe('handleSendMessage', () => {
});
});
});
-
- describe('When redirection is not allowed', () => {
- beforeEach(() => {
- mockRpcQueueManagerGetId.mockReturnValue('1');
- mockCanRedirect.mockReturnValue(false);
- });
- it('should skip goBack if canRedirect is false', async () => {
- await handleSendMessage({
- msg: {
- data: {
- id: 1,
- },
- },
- connection: mockConnection,
- });
-
- expect(mockMinimizer.goBack).not.toHaveBeenCalled();
- });
- it('should skip goBack if trigger is not deeplink', async () => {
- mockConnection.trigger = 'resume';
-
- await handleSendMessage({
- msg: {
- data: {
- id: 1,
- },
- },
- connection: mockConnection,
- });
-
- expect(mockMinimizer.goBack).not.toHaveBeenCalled();
- });
- });
});
describe('Final state update', () => {
diff --git a/app/core/SDKConnect/handlers/handleSendMessage.ts b/app/core/SDKConnect/handlers/handleSendMessage.ts
index d8790dfa0df..322e61e0712 100644
--- a/app/core/SDKConnect/handlers/handleSendMessage.ts
+++ b/app/core/SDKConnect/handlers/handleSendMessage.ts
@@ -1,9 +1,6 @@
-import { Platform } from 'react-native';
import Routes from '../../../../app/constants/navigation/Routes';
import AppConstants from '../../../../app/core/AppConstants';
import Logger from '../../../util/Logger';
-import Device from '../../../util/device';
-import { Minimizer } from '../../NativeModules';
import { Connection } from '../Connection';
import { METHODS_TO_DELAY, RPC_METHODS } from '../SDKConnectConstants';
import DevLogger from '../utils/DevLogger';
@@ -140,16 +137,9 @@ export const handleSendMessage = async ({
// Trigger should be removed after redirect so we don't redirect the dapp next time and go back to nothing.
connection.trigger = 'resume';
-
- // Check for iOS 17 and above to use a custom modal, as Minimizer.goBack() is incompatible with these versions
- if (Device.isIos() && parseInt(Platform.Version as string) >= 17) {
- connection.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
- screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
- });
- } else {
- DevLogger.log(`[handleSendMessage] goBack()`);
- await Minimizer.goBack();
- }
+ connection.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.RETURN_TO_DAPP_MODAL,
+ });
} catch (err) {
Logger.log(
err,
diff --git a/app/core/SnapKeyring/BitcoinWalletSnap.ts b/app/core/SnapKeyring/BitcoinWalletSnap.ts
index b3da42ea99f..8c9780ca7f7 100644
--- a/app/core/SnapKeyring/BitcoinWalletSnap.ts
+++ b/app/core/SnapKeyring/BitcoinWalletSnap.ts
@@ -1,3 +1,4 @@
+///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import { SnapId } from '@metamask/snaps-sdk';
import { Sender } from '@metamask/keyring-snap-client';
import { HandlerType } from '@metamask/snaps-utils';
@@ -28,3 +29,4 @@ export class BitcoinWalletSnapSender implements Sender {
request,
})) as Json;
}
+///: END:ONLY_INCLUDE_IF
diff --git a/app/core/SnapKeyring/SolanaWalletSnap.ts b/app/core/SnapKeyring/SolanaWalletSnap.ts
new file mode 100644
index 00000000000..d95a0b3a170
--- /dev/null
+++ b/app/core/SnapKeyring/SolanaWalletSnap.ts
@@ -0,0 +1,31 @@
+///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
+import { SnapId } from '@metamask/snaps-sdk';
+import { Sender } from '@metamask/keyring-snap-client';
+import { HandlerType } from '@metamask/snaps-utils';
+import { Json, JsonRpcRequest } from '@metamask/utils';
+// This dependency is still installed as part of the `package.json`, however
+// the Snap is being pre-installed only for Flask build (for the moment).
+import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json';
+import { handleSnapRequest } from '../Snaps/utils';
+import Engine from '../Engine';
+
+export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId;
+
+export const SOLANA_WALLET_NAME: string =
+ SolanaWalletSnap.manifest.proposedName;
+
+const controllerMessenger = Engine.controllerMessenger;
+
+export class SolanaWalletSnapSender implements Sender {
+ // We assume the caller of this module is aware of this. If we try to use this module
+ // without having the pre-installed Snap, this will likely throw an error in
+ // the `handleSnapRequest` action.
+ send = async (request: JsonRpcRequest): Promise =>
+ (await handleSnapRequest(controllerMessenger, {
+ origin: 'metamask',
+ snapId: SOLANA_WALLET_SNAP_ID,
+ handler: HandlerType.OnKeyringRequest,
+ request,
+ })) as Json;
+}
+///: END:ONLY_INCLUDE_IF
diff --git a/app/images/solana-logo.png b/app/images/solana-logo.png
new file mode 100644
index 00000000000..546c9d797ba
Binary files /dev/null and b/app/images/solana-logo.png differ
diff --git a/app/lib/ppom/ppom-util.test.ts b/app/lib/ppom/ppom-util.test.ts
index 6d0ab6bc03d..5eb6ade7406 100644
--- a/app/lib/ppom/ppom-util.test.ts
+++ b/app/lib/ppom/ppom-util.test.ts
@@ -480,4 +480,14 @@ describe('PPOM Utils', () => {
},
);
});
+
+ describe('clearSignatureSecurityAlertResponse', () => {
+ it('set call action to set securityAlertResponse for signature in redux state to undefined', async () => {
+ const spy = jest.spyOn(SignatureRequestActions, 'default');
+ PPOMUtil.clearSignatureSecurityAlertResponse();
+ expect(spy).toHaveBeenCalledTimes(1);
+ // function call with no arguments
+ expect(spy).toHaveBeenCalledWith();
+ });
+ });
});
diff --git a/app/lib/ppom/ppom-util.ts b/app/lib/ppom/ppom-util.ts
index b76d4bf8f4b..14c3997107b 100644
--- a/app/lib/ppom/ppom-util.ts
+++ b/app/lib/ppom/ppom-util.ts
@@ -251,4 +251,12 @@ function normalizeRequest(request: PPOMRequest): PPOMRequest {
};
}
-export default { validateRequest, isChainSupported };
+function clearSignatureSecurityAlertResponse() {
+ store.dispatch(setSignatureRequestSecurityAlertResponse());
+}
+
+export default {
+ validateRequest,
+ isChainSupported,
+ clearSignatureSecurityAlertResponse,
+};
diff --git a/app/lib/snaps/preinstalled-snaps.ts b/app/lib/snaps/preinstalled-snaps.ts
index eee160e1148..6b81b326ca1 100644
--- a/app/lib/snaps/preinstalled-snaps.ts
+++ b/app/lib/snaps/preinstalled-snaps.ts
@@ -1,13 +1,15 @@
import type { PreinstalledSnap } from '@metamask/snaps-controllers';
import MessageSigningSnap from '@metamask/message-signing-snap/dist/preinstalled-snap.json';
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
-import BitcoinSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json';
+import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json';
+import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json';
///: END:ONLY_INCLUDE_IF
const PREINSTALLED_SNAPS: readonly PreinstalledSnap[] = Object.freeze([
MessageSigningSnap as PreinstalledSnap,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
- BitcoinSnap as unknown as PreinstalledSnap,
+ BitcoinWalletSnap as unknown as PreinstalledSnap,
+ SolanaWalletSnap as unknown as PreinstalledSnap,
///: END:ONLY_INCLUDE_IF
]);
diff --git a/app/reducers/multichain/index.ts b/app/reducers/multichain/index.ts
index 667897d9023..64027883d97 100644
--- a/app/reducers/multichain/index.ts
+++ b/app/reducers/multichain/index.ts
@@ -1,10 +1,13 @@
+///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import { MultichainSettingsState } from '../../actions/multichain/state';
export const initialState: MultichainSettingsState = {
- bitcoinSupportEnabled: false,
- bitcoinTestnetSupportEnabled: false,
+ bitcoinSupportEnabled: true,
+ bitcoinTestnetSupportEnabled: true,
+ solanaSupportEnabled: true,
};
const multichainReducer = (state = initialState) => state;
export default multichainReducer;
+///: END:ONLY_INCLUDE_IF
diff --git a/app/selectors/accountsController.test.ts b/app/selectors/accountsController.test.ts
index 92598446eef..3c33cdf3af9 100644
--- a/app/selectors/accountsController.test.ts
+++ b/app/selectors/accountsController.test.ts
@@ -192,42 +192,42 @@ describe('Accounts Controller Selectors', () => {
});
});
-describe('Bitcoin Account Selectors', () => {
- function getStateWithAccount(account: InternalAccount) {
- return {
- engine: {
- backgroundState: {
- AccountsController: {
- internalAccounts: {
- accounts: {
- [account.id]: account,
- },
- selectedAccount: account.id,
+const MOCK_BTC_MAINNET_ADDRESS = 'bc1qkv7xptmd7ejmnnd399z9p643updvula5j4g4nd';
+const MOCK_BTC_TESTNET_ADDRESS = 'tb1q63st8zfndjh00gf9hmhsdg7l8umuxudrj4lucp';
+
+function getStateWithAccount(account: InternalAccount) {
+ return {
+ engine: {
+ backgroundState: {
+ AccountsController: {
+ internalAccounts: {
+ accounts: {
+ [account.id]: account,
},
+ selectedAccount: account.id,
},
- KeyringController: MOCK_KEYRING_CONTROLLER,
},
+ KeyringController: MOCK_KEYRING_CONTROLLER,
},
- } as RootState;
- }
-
- const MOCK_BTC_MAINNET_ADDRESS = 'bc1qkv7xptmd7ejmnnd399z9p643updvula5j4g4nd';
- const MOCK_BTC_TESTNET_ADDRESS = 'tb1q63st8zfndjh00gf9hmhsdg7l8umuxudrj4lucp';
+ },
+ } as RootState;
+}
- const btcMainnetAccount = createMockInternalAccount(
- MOCK_BTC_MAINNET_ADDRESS,
- 'Bitcoin Account',
- KeyringTypes.snap,
- BtcAccountType.P2wpkh,
- );
+const btcMainnetAccount = createMockInternalAccount(
+ MOCK_BTC_MAINNET_ADDRESS,
+ 'Bitcoin Account',
+ KeyringTypes.snap,
+ BtcAccountType.P2wpkh,
+);
- const btcTestnetAccount = createMockInternalAccount(
- MOCK_BTC_TESTNET_ADDRESS,
- 'Bitcoin Testnet Account',
- KeyringTypes.snap,
- BtcAccountType.P2wpkh,
- );
+const btcTestnetAccount = createMockInternalAccount(
+ MOCK_BTC_TESTNET_ADDRESS,
+ 'Bitcoin Testnet Account',
+ KeyringTypes.snap,
+ BtcAccountType.P2wpkh,
+);
+describe('Bitcoin Account Selectors', () => {
describe('hasCreatedBtcMainnetAccount', () => {
it('returns true when a BTC mainnet account exists', () => {
const state = getStateWithAccount(btcMainnetAccount);
diff --git a/app/selectors/browser.ts b/app/selectors/browser.ts
new file mode 100644
index 00000000000..458cf90d6ba
--- /dev/null
+++ b/app/selectors/browser.ts
@@ -0,0 +1,17 @@
+import { RootState } from '../reducers';
+import { createDeepEqualSelector } from './util';
+
+interface SiteItem {
+ url: string;
+ name: string;
+}
+
+export const selectBrowserHistoryWithType = createDeepEqualSelector(
+ (state: RootState) => state.browser.history,
+ (history: SiteItem[]) => history.map(item => ({...item, type: 'recents'})).reverse()
+);
+
+export const selectBrowserBookmarksWithType = createDeepEqualSelector(
+ (state: RootState) => state.bookmarks,
+ (bookmarks: SiteItem[]) => bookmarks.map(item => ({...item, type: 'favorites'}))
+);
\ No newline at end of file
diff --git a/app/selectors/multichain.test.ts b/app/selectors/multichain.test.ts
index 276517a18cd..ca00cdec667 100644
--- a/app/selectors/multichain.test.ts
+++ b/app/selectors/multichain.test.ts
@@ -4,6 +4,7 @@ import {
selectAccountTokensAcrossChains,
selectIsBitcoinSupportEnabled,
selectIsBitcoinTestnetSupportEnabled,
+ selectIsSolanaSupportEnabled,
} from './multichain';
describe('Multichain Selectors', () => {
@@ -92,6 +93,7 @@ describe('Multichain Selectors', () => {
multichainSettings: {
bitcoinSupportEnabled: true,
bitcoinTestnetSupportEnabled: false,
+ solanaSupportEnabled: true,
},
} as unknown as RootState;
@@ -176,7 +178,7 @@ describe('Multichain Selectors', () => {
});
});
- describe('Bitcoin Support Flags', () => {
+ describe('Multichain Support Flags', () => {
it('should return bitcoin support enabled state', () => {
expect(selectIsBitcoinSupportEnabled(mockState)).toBe(true);
});
@@ -184,5 +186,8 @@ describe('Multichain Selectors', () => {
it('should return bitcoin testnet support enabled state', () => {
expect(selectIsBitcoinTestnetSupportEnabled(mockState)).toBe(false);
});
+ it('should return Solana support enabled state', () => {
+ expect(selectIsSolanaSupportEnabled(mockState)).toBe(true);
+ });
});
});
diff --git a/app/selectors/multichain.ts b/app/selectors/multichain.ts
index c02a017316f..08ee52a744c 100644
--- a/app/selectors/multichain.ts
+++ b/app/selectors/multichain.ts
@@ -1,7 +1,6 @@
import { createSelector } from 'reselect';
import { Hex } from '@metamask/utils';
import { Token, getNativeTokenAddress } from '@metamask/assets-controllers';
-import { RootState } from '../reducers';
import {
selectSelectedInternalAccountFormattedAddress,
selectSelectedInternalAccount,
@@ -17,6 +16,10 @@ import {
selectCurrentCurrency,
} from './currencyRateController';
+///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
+import { RootState } from '../reducers';
+///: END:ONLY_INCLUDE_IF
+
interface NativeTokenBalance {
balance: string;
stakedBalance: string;
@@ -220,6 +223,7 @@ export const selectAccountTokensAcrossChains = createSelector(
},
);
+///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
/**
* Get the state of the `bitcoinSupportEnabled` flag.
*
@@ -239,3 +243,14 @@ export function selectIsBitcoinSupportEnabled(state: RootState) {
export function selectIsBitcoinTestnetSupportEnabled(state: RootState) {
return state.multichainSettings.bitcoinTestnetSupportEnabled;
}
+
+/**
+ * Get the state of the `solanaSupportEnabled` flag.
+ *
+ * @param {*} state
+ * @returns The state of the `solanaSupportEnabled` flag.
+ */
+export function selectIsSolanaSupportEnabled(state: RootState) {
+ return state.multichainSettings.solanaSupportEnabled;
+}
+///: END:ONLY_INCLUDE_IF
diff --git a/app/store/index.ts b/app/store/index.ts
index 7dca2b0e1c8..03e433ba75d 100644
--- a/app/store/index.ts
+++ b/app/store/index.ts
@@ -1,4 +1,4 @@
-import { Store } from 'redux';
+import { AnyAction } from 'redux';
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import createSagaMiddleware from 'redux-saga';
@@ -12,20 +12,17 @@ import thunk from 'redux-thunk';
import persistConfig from './persistConfig';
import getUIStartupSpan from '../core/Performance/UIStartup';
-import ReduxService from '../core/redux';
+import ReduxService, { ReduxStore } from '../core/redux';
import { onPersistedDataLoaded } from '../actions/user';
-import { validatePostMigrationState } from './validateMigration/validateMigration';
-// TODO: Improve type safety by using real Action types instead of `any`
-// TODO: Replace "any" with type
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const pReducer = persistReducer(persistConfig, rootReducer);
+// TODO: Improve type safety by using real Action types instead of `AnyAction`
+const pReducer = persistReducer(
+ persistConfig,
+ rootReducer,
+);
-// TODO: Fix the Action type. It's set to `any` now because some of the
-// TypeScript reducers have invalid actions
-// TODO: Replace "any" with type
-// eslint-disable-next-line @typescript-eslint/no-explicit-any, import/no-mutable-exports
-let store: Store, persistor;
+// eslint-disable-next-line import/no-mutable-exports
+let store: ReduxStore, persistor;
const createStoreAndPersistor = async () => {
trace({
name: TraceName.StoreInit,
@@ -70,10 +67,6 @@ const createStoreAndPersistor = async () => {
endTrace({ name: TraceName.StoreInit });
// Signal that persisted data has been loaded
store.dispatch(onPersistedDataLoaded());
-
- // validate the state after migration
- const currentState = store.getState();
- validatePostMigrationState(currentState);
};
persistor = persistStore(store, null, onPersistComplete);
diff --git a/app/store/migrations/index.test.ts b/app/store/migrations/index.test.ts
index ce765e41efb..e3653f04df8 100644
--- a/app/store/migrations/index.test.ts
+++ b/app/store/migrations/index.test.ts
@@ -71,6 +71,37 @@ describe('asyncifyMigrations', () => {
expect(isPromiseMigrations).toEqual(true);
});
+
+ it('should only call validation callback after all migrations complete', async () => {
+ const mockValidation = jest.fn();
+ const testMigrationList = {
+ 0: synchronousMigration,
+ 1: asyncMigration,
+ 2: synchronousMigration,
+ };
+
+ // Convert all migrations to async with validation callback
+ const asyncMigrations = asyncifyMigrations(
+ testMigrationList,
+ mockValidation,
+ );
+
+ // Run migrations in sequence and verify validation is only called after the highest migration
+ let state: PersistedState = initialState;
+
+ for (const migrationKey in asyncMigrations) {
+ state = (await asyncMigrations[migrationKey](state)) as PersistedState;
+
+ if (Number(migrationKey) === 2) {
+ // Should be called exactly once after the last migration
+ expect(mockValidation).toHaveBeenCalledTimes(1);
+ expect(mockValidation).toHaveBeenCalledWith(state);
+ } else {
+ // Should not be called for any other migration
+ expect(mockValidation).not.toHaveBeenCalled();
+ }
+ }
+ });
});
describe('migrations', () => {
diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts
index 43d15139357..94522a41af3 100644
--- a/app/store/migrations/index.ts
+++ b/app/store/migrations/index.ts
@@ -67,6 +67,8 @@ import migration63 from './063';
import migration64 from './064';
import migration65 from './065';
import migration66 from './066';
+import { validatePostMigrationState } from '../validateMigration/validateMigration';
+import { RootState } from '../../reducers';
type MigrationFunction = (state: unknown) => unknown;
type AsyncMigrationFunction = (state: unknown) => Promise;
@@ -149,7 +151,10 @@ export const migrationList: MigrationsList = {
};
// Enable both synchronous and asynchronous migrations
-export const asyncifyMigrations = (inputMigrations: MigrationsList) =>
+export const asyncifyMigrations = (
+ inputMigrations: MigrationsList,
+ onMigrationsComplete?: (state: unknown) => void,
+) =>
Object.entries(inputMigrations).reduce(
(newMigrations, [migrationNumber, migrationFunction]) => {
// Handle migrations as async
@@ -157,7 +162,17 @@ export const asyncifyMigrations = (inputMigrations: MigrationsList) =>
incomingState: Promise | unknown,
) => {
const state = await incomingState;
- return migrationFunction(state);
+ const migratedState = await migrationFunction(state);
+
+ // If this is the last migration and we have a callback, run it
+ if (
+ onMigrationsComplete &&
+ Number(migrationNumber) === Object.keys(inputMigrations).length - 1
+ ) {
+ onMigrationsComplete(migratedState);
+ }
+
+ return migratedState;
};
newMigrations[migrationNumber] = asyncMigration;
return newMigrations;
@@ -166,9 +181,9 @@ export const asyncifyMigrations = (inputMigrations: MigrationsList) =>
);
// Convert all migrations to async
-export const migrations = asyncifyMigrations(
- migrationList,
-) as unknown as MigrationManifest;
+export const migrations = asyncifyMigrations(migrationList, (state) => {
+ validatePostMigrationState(state as RootState);
+}) as unknown as MigrationManifest;
// The latest (i.e. highest) version number.
export const version = Object.keys(migrations).length - 1;
diff --git a/app/store/persistConfig.ts b/app/store/persistConfig.ts
index 5b8b4358134..81159b944aa 100644
--- a/app/store/persistConfig.ts
+++ b/app/store/persistConfig.ts
@@ -3,7 +3,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import FilesystemStorage from 'redux-persist-filesystem-storage';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
import { RootState } from '../reducers';
-import { migrations, version } from './migrations';
+import { version, migrations } from './migrations';
import Logger from '../util/Logger';
import Device from '../util/device';
import { UserState } from '../reducers/user';
diff --git a/app/store/validateMigration/validateMigration.test.ts b/app/store/validateMigration/validateMigration.test.ts
index 32375b18917..2672cfcb18b 100644
--- a/app/store/validateMigration/validateMigration.test.ts
+++ b/app/store/validateMigration/validateMigration.test.ts
@@ -7,6 +7,7 @@ import { validateEngineInitialized } from './engineBackgroundState';
jest.mock('../../util/Logger', () => ({
error: jest.fn(),
+ log: jest.fn(),
}));
jest.mock('./accountsController');
@@ -23,6 +24,14 @@ describe('validatePostMigrationState', () => {
(validateKeyringController as jest.Mock).mockReturnValue([]);
});
+ it('logs when validation starts', () => {
+ const mockState = {} as RootState;
+ validatePostMigrationState(mockState);
+
+ expect(Logger.log).toHaveBeenCalledWith('Migration validation started');
+ expect(Logger.log).toHaveBeenCalledTimes(1);
+ });
+
it('runs all validation checks', () => {
const mockState = {} as RootState;
validatePostMigrationState(mockState);
diff --git a/app/store/validateMigration/validateMigration.ts b/app/store/validateMigration/validateMigration.ts
index 0638d65b680..36b1ea508ed 100644
--- a/app/store/validateMigration/validateMigration.ts
+++ b/app/store/validateMigration/validateMigration.ts
@@ -18,6 +18,7 @@ const checks: ValidationCheck[] = [
* This makes sure your app keeps running even if some data is unexpected.
*/
export function validatePostMigrationState(state: RootState): void {
+ Logger.log('Migration validation started');
const allErrors = checks.flatMap((check) => check(state));
// If there are any errors, log them
diff --git a/app/util/dapp-url-list.js b/app/util/dapp-url-list.js
index edc63c334c3..cfb9ddab8d4 100644
--- a/app/util/dapp-url-list.js
+++ b/app/util/dapp-url-list.js
@@ -179,4 +179,304 @@ export default [
url: 'https://3box.io/',
name: '3Box',
},
+ {
+ url: 'https://simpleswap.io/',
+ name: 'SimpleSwap',
+ },
+ {
+ url: 'https://www.orbiter.finance/',
+ name: 'Orbiter',
+ },
+ {
+ url: 'https://stargate.finance/',
+ name: 'Stargate',
+ },
+ {
+ url: 'https://www.phosphor.xyz/',
+ name: 'Phosphor',
+ },
+ {
+ url: 'https://nftkt.io/wgdapp',
+ name: 'NiftyKit',
+ },
+ {
+ url: 'https://manifold.xyz/',
+ name: 'Manifold',
+ },
+ {
+ url: 'https://bueno.art/',
+ name: 'Bueno',
+ },
+ {
+ url: 'https://www.lens.xyz/',
+ name: 'Lens',
+ },
+ {
+ url: 'https://poap.xyz/',
+ name: 'POAP',
+ },
+ {
+ url: 'https://www.alphabot.app/',
+ name: 'Alphabot',
+ },
+ {
+ url: 'https://www.premint.xyz/',
+ name: 'Premint',
+ },
+ {
+ url: 'https://tokenproof.xyz/',
+ name: 'tokenproof',
+ },
+ {
+ url: 'https://www.collab.land/',
+ name: 'Collab Land',
+ },
+ {
+ url: 'https://www.arweave.org/',
+ name: 'Arweave',
+ },
+ {
+ url: 'https://www.ipfs.tech',
+ name: 'IPFS',
+ },
+ {
+ url: 'https://apecoin.com/',
+ name: 'ApeCoin',
+ },
+ {
+ url: 'https://daomaker.com/',
+ name: 'DAO Maker',
+ },
+ {
+ url: 'https://snapshot.org/#/',
+ name: 'Snapshot',
+ },
+ {
+ url: 'https://parallel.fi',
+ name: 'Parallel',
+ },
+ {
+ url: 'https://www.usecyan.com/',
+ name: 'CYAN',
+ },
+ {
+ url: 'https://blur.io/',
+ name: 'Blur',
+ },
+ {
+ url: 'https://foundation.app/',
+ name: 'Foundation',
+ },
+ {
+ url: 'https://looksrare.org/',
+ name: 'LooksRare',
+ },
+ {
+ url: 'https://magiceden.io/',
+ name: 'Magic Eden',
+ },
+ {
+ url: 'https://makersplace.com/',
+ name: 'Makersplace',
+ },
+ {
+ url: 'https://www.niftygateway.com/',
+ name: 'Nifty Gateway',
+ },
+ {
+ url: 'https://solanart.io/',
+ name: 'Solanart',
+ },
+ {
+ url: 'https://sound.xyz/',
+ name: 'Sound',
+ },
+ {
+ url: 'https://unshorten.it/',
+ name: 'Unshorten.It!',
+ },
+ {
+ url: 'https://www.browserling.com/',
+ name: 'Browserling',
+ },
+ {
+ url: 'https://urlscan.io/',
+ name: 'URLScan',
+ },
+ {
+ url: 'https://www.virustotal.com/',
+ name: 'VirusTotal',
+ },
+ {
+ url: 'https://www.flashbots.net/',
+ name: 'Flashbots',
+ },
+ {
+ url: 'https://revoke.cash/',
+ name: 'Revoke Cash',
+ },
+ {
+ url: 'https://honeypot.is/',
+ name: 'Honeypot.is',
+ },
+ {
+ url: 'https://boringsecurity.com/',
+ name: 'Boring Security',
+ },
+ {
+ url: 'https://gopluslabs.io/',
+ name: 'GoPlus Security',
+ },
+ {
+ url: 'https://bitwarden.com/',
+ name: 'Bitwarden',
+ },
+ {
+ url: 'https://authy.com/',
+ name: 'Authy',
+ },
+ {
+ url: 'https://ublockorigin.com/',
+ name: 'uBlock Origin',
+ },
+ {
+ url: 'https://brave.com/',
+ name: 'Brave',
+ },
+ {
+ url: 'https://www.malwarebytes.com/',
+ name: 'Malwarebytes',
+ },
+ {
+ url: 'https://mullvad.net/',
+ name: 'Mullvad',
+ },
+ {
+ url: 'https://warpcast.com/',
+ name: 'Warpcast',
+ },
+ {
+ url: 'https://other.page/',
+ name: 'Other Page',
+ },
+ {
+ url: 'https://soulbound.gg/',
+ name: 'Soulbound',
+ },
+ {
+ url: 'https://niftytailor.com/',
+ name: 'Nifty Tailor',
+ },
+ {
+ url: 'https://www.walletchat.fun/',
+ name: 'WalletChat',
+ },
+ {
+ url: 'https://www.sending.network/',
+ name: 'Sending Network',
+ },
+ {
+ url: 'https://cyberconnect.me/',
+ name: 'CyberConnect',
+ },
+ {
+ url: 'https://pump.fun/',
+ name: 'Pump.fun',
+ },
+ {
+ url: 'https://coinstats.app/',
+ name: 'CoinStats',
+ },
+ {
+ url: 'https://photon.tinyastro.io/',
+ name: 'Photon',
+ },
+ {
+ url: 'https://nftgo.io/',
+ name: 'NFTGo',
+ },
+ {
+ url: 'https://rarity.tools/',
+ name: 'Rarity Tools',
+ },
+ {
+ url: 'https://www.nansen.ai/',
+ name: 'Nansen',
+ },
+ {
+ url: 'https://dune.com/',
+ name: 'Dune',
+ },
+ {
+ url: 'https://www.dextools.io/',
+ name: 'DEXTools',
+ },
+ {
+ url: 'https://coinmarketcap.com/',
+ name: 'CoinMarketCap',
+ },
+ {
+ url: 'https://www.coingecko.com/',
+ name: 'CoinGecko',
+ },
+ {
+ url: 'https://catchmint.xyz/',
+ name: 'Catchmint',
+ },
+ {
+ url: 'https://dappradar.com/',
+ name: 'DappRadar',
+ },
+ {
+ url: 'https://balancer.fi/',
+ name: 'Balancer',
+ },
+ {
+ url: 'https://www.ninjalerts.com/',
+ name: 'Ninjalerts',
+ },
+ {
+ url: 'https://defillama.com/',
+ name: 'DefiLlama',
+ },
+ {
+ url: 'https://dexscreener.com/',
+ name: 'DEX Screener',
+ },
+ {
+ url: 'https://aave.com/',
+ name: 'Aave',
+ },
+ {
+ url: 'https://gokhshteinmedia.com/',
+ name: 'Gokhshtein Media',
+ },
+ {
+ url: 'https://decrypt.co/',
+ name: 'Decrypt',
+ },
+ {
+ url: 'https://www.coindesk.com/',
+ name: 'CoinDesk',
+ },
+ {
+ url: 'https://cointelegraph.com/',
+ name: 'CoinTelegraph',
+ },
+ {
+ url: 'https://www.theblock.co/',
+ name: 'The Block',
+ },
+ {
+ url: 'https://blockworks.co/',
+ name: 'Blockworks',
+ },
+ {
+ url: 'https://www.bankless.com/',
+ name: 'Bankless',
+ },
+ {
+ url: 'https://nftnow.com/',
+ name: 'nft now',
+ },
];
diff --git a/app/util/networks/index.js b/app/util/networks/index.js
index 461f8ea6b87..bd03e6f8c8a 100644
--- a/app/util/networks/index.js
+++ b/app/util/networks/index.js
@@ -499,3 +499,5 @@ export const isPermissionsSettingsV1Enabled =
export const isPortfolioViewEnabled = () =>
process.env.PORTFOLIO_VIEW === 'true';
+
+export const isMultichainV1Enabled = () => process.env.MULTICHAIN_V1 === 'true';
diff --git a/app/util/sentry/__snapshots__/utils.test.ts.snap b/app/util/sentry/__snapshots__/utils.test.ts.snap
index 8af1d0305c7..01e4488c0b5 100644
--- a/app/util/sentry/__snapshots__/utils.test.ts.snap
+++ b/app/util/sentry/__snapshots__/utils.test.ts.snap
@@ -52,6 +52,9 @@ exports[`captureSentryFeedback maskObject masks initial root state fixture 1`] =
"eth_signTypedData_v4",
],
"options": {},
+ "scopes": [
+ "eip155",
+ ],
"type": "eip155:eoa",
},
"2be55f5b-eba9-41a7-a9ed-a6a8274aca28": {
@@ -70,6 +73,9 @@ exports[`captureSentryFeedback maskObject masks initial root state fixture 1`] =
"eth_signTransaction",
],
"options": {},
+ "scopes": [
+ "eip155",
+ ],
"type": "eip155:eoa",
},
},
diff --git a/app/util/sentry/utils.js b/app/util/sentry/utils.js
index 0208f65eb32..a3c722547ff 100644
--- a/app/util/sentry/utils.js
+++ b/app/util/sentry/utils.js
@@ -39,6 +39,7 @@ export const sentryStateMask = {
type: true,
options: true,
methods: true,
+ scopes: true,
metadata: {
name: true,
importTime: true,
diff --git a/app/util/sentry/utils.test.ts b/app/util/sentry/utils.test.ts
index 32071388370..e17bc88224d 100644
--- a/app/util/sentry/utils.test.ts
+++ b/app/util/sentry/utils.test.ts
@@ -11,6 +11,7 @@ import {
import { DeepPartial } from '../test/renderWithProvider';
import { RootState } from '../../reducers';
import { NetworkStatus } from '@metamask/network-controller';
+import { EthScopes } from '@metamask/keyring-api';
jest.mock('@sentry/react-native', () => ({
...jest.requireActual('@sentry/react-native'),
@@ -177,6 +178,7 @@ describe('captureSentryFeedback', () => {
'eth_signTypedData_v3',
'eth_signTypedData_v4',
],
+ scopes: [EthScopes.Namespace],
options: {},
type: 'eip155:eoa',
},
@@ -191,6 +193,7 @@ describe('captureSentryFeedback', () => {
lastSelected: 1720023898237,
name: 'Account 2',
},
+ scopes: [EthScopes.Namespace],
methods: ['personal_sign', 'eth_signTransaction'],
options: {},
type: 'eip155:eoa',
@@ -630,6 +633,7 @@ describe('captureSentryFeedback', () => {
'eth_signTypedData_v3',
'eth_signTypedData_v4',
]);
+ expect(maskedAccount1.scopes).toEqual([EthScopes.Namespace]);
expect(maskedAccount1.metadata).toEqual({
importTime: 1720023898234,
keyring: { type: 'HD Key Tree' },
@@ -645,6 +649,7 @@ describe('captureSentryFeedback', () => {
'personal_sign',
'eth_signTransaction',
]);
+ expect(maskedAccount2.scopes).toEqual([EthScopes.Namespace]);
expect(maskedAccount2.metadata).toEqual({
importTime: 1720023898235,
keyring: { type: 'HD Key Tree' },
diff --git a/app/util/theme/models.ts b/app/util/theme/models.ts
index 51912ab98b1..045ef5c5a05 100644
--- a/app/util/theme/models.ts
+++ b/app/util/theme/models.ts
@@ -1,5 +1,7 @@
-import type { Theme as DesignTokenTheme } from '@metamask/design-tokens';
-import type { BrandColor } from '@metamask/design-tokens/dist/types/js/brandColor/brandColor.types';
+import type {
+ Theme as DesignTokenTheme,
+ BrandColor,
+} from '@metamask/design-tokens';
export enum AppThemeKey {
os = 'os',
diff --git a/attribution.txt b/attribution.txt
index 9ec633fe621..6ddde27398c 100644
--- a/attribution.txt
+++ b/attribution.txt
@@ -1,9 +1,36 @@
+@0no-co/graphql.web
+1.0.13
+MIT License
+
+Copyright (c) 0no.co
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+******************************
+
abort-controller
3.0.0
MIT License
Copyright (c) 2017 Toru Nagashima
-
+s
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
@@ -223,6 +250,49 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+******************************
+
+aggregate-error
+3.1.0
+MIT License
+
+Copyright (c) Sindre Sorhus (sindresorhus.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+******************************
+
+ajv
+8.17.1
+The MIT License (MIT)
+
+Copyright (c) 2015-2021 Evgeny Poberezkin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+
******************************
@ampproject/remapping
@@ -458,6 +528,21 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+******************************
+
+ansi-escapes
+4.3.2
+MIT License
+
+Copyright (c) Sindre Sorhus (https://sindresorhus.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
******************************
ansi-fragments
@@ -591,6 +676,31 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+******************************
+
+any-promise
+1.3.0
+Copyright (C) 2014-2016 Kevin Beaty
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
******************************
apg-js
@@ -1667,6 +1777,33 @@ limitations under the License.
limitations under the License.
+******************************
+
+application-config-path
+0.1.1
+MIT License
+
+Copyright (c) 2015 Linus Unnebäck
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
******************************
aproba
@@ -1744,27 +1881,27 @@ OTHER DEALINGS IN THE SOFTWARE.
archiver-utils
2.1.0
-Copyright (c) 2015 Chris Talkington.
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+Copyright (c) 2015 Chris Talkington.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
******************************
@@ -1791,6 +1928,33 @@ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
USE OR PERFORMANCE OF THIS SOFTWARE.
+******************************
+
+arg
+4.1.0
+MIT License
+
+Copyright (c) 2017 Zeit, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
******************************
argparse
@@ -1818,6 +1982,266 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+******************************
+
+argparse
+2.0.1
+A. HISTORY OF THE SOFTWARE
+==========================
+
+Python was created in the early 1990s by Guido van Rossum at Stichting
+Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
+as a successor of a language called ABC. Guido remains Python's
+principal author, although it includes many contributions from others.
+
+In 1995, Guido continued his work on Python at the Corporation for
+National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
+in Reston, Virginia where he released several versions of the
+software.
+
+In May 2000, Guido and the Python core development team moved to
+BeOpen.com to form the BeOpen PythonLabs team. In October of the same
+year, the PythonLabs team moved to Digital Creations, which became
+Zope Corporation. In 2001, the Python Software Foundation (PSF, see
+https://www.python.org/psf/) was formed, a non-profit organization
+created specifically to own Python-related Intellectual Property.
+Zope Corporation was a sponsoring member of the PSF.
+
+All Python releases are Open Source (see http://www.opensource.org for
+the Open Source Definition). Historically, most, but not all, Python
+releases have also been GPL-compatible; the table below summarizes
+the various releases.
+
+ Release Derived Year Owner GPL-
+ from compatible? (1)
+
+ 0.9.0 thru 1.2 1991-1995 CWI yes
+ 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
+ 1.6 1.5.2 2000 CNRI no
+ 2.0 1.6 2000 BeOpen.com no
+ 1.6.1 1.6 2001 CNRI yes (2)
+ 2.1 2.0+1.6.1 2001 PSF no
+ 2.0.1 2.0+1.6.1 2001 PSF yes
+ 2.1.1 2.1+2.0.1 2001 PSF yes
+ 2.1.2 2.1.1 2002 PSF yes
+ 2.1.3 2.1.2 2002 PSF yes
+ 2.2 and above 2.1.1 2001-now PSF yes
+
+Footnotes:
+
+(1) GPL-compatible doesn't mean that we're distributing Python under
+ the GPL. All Python licenses, unlike the GPL, let you distribute
+ a modified version without making your changes open source. The
+ GPL-compatible licenses make it possible to combine Python with
+ other software that is released under the GPL; the others don't.
+
+(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
+ because its license has a choice of law clause. According to
+ CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
+ is "not incompatible" with the GPL.
+
+Thanks to the many outside volunteers who have worked under Guido's
+direction to make these releases possible.
+
+
+B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
+===============================================================
+
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation;
+All Rights Reserved" are retained in Python alone or in any derivative version
+prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
+-------------------------------------------
+
+BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
+
+1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
+office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
+Individual or Organization ("Licensee") accessing and otherwise using
+this software in source or binary form and its associated
+documentation ("the Software").
+
+2. Subject to the terms and conditions of this BeOpen Python License
+Agreement, BeOpen hereby grants Licensee a non-exclusive,
+royalty-free, world-wide license to reproduce, analyze, test, perform
+and/or display publicly, prepare derivative works, distribute, and
+otherwise use the Software alone or in any derivative version,
+provided, however, that the BeOpen Python License is retained in the
+Software, alone or in any derivative version prepared by Licensee.
+
+3. BeOpen is making the Software available to Licensee on an "AS IS"
+basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
+SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
+AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
+DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+5. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+6. This License Agreement shall be governed by and interpreted in all
+respects by the law of the State of California, excluding conflict of
+law provisions. Nothing in this License Agreement shall be deemed to
+create any relationship of agency, partnership, or joint venture
+between BeOpen and Licensee. This License Agreement does not grant
+permission to use BeOpen trademarks or trade names in a trademark
+sense to endorse or promote products or services of Licensee, or any
+third party. As an exception, the "BeOpen Python" logos available at
+http://www.pythonlabs.com/logos.html may be used according to the
+permissions granted on that web page.
+
+7. By copying, installing or otherwise using the software, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
+---------------------------------------
+
+1. This LICENSE AGREEMENT is between the Corporation for National
+Research Initiatives, having an office at 1895 Preston White Drive,
+Reston, VA 20191 ("CNRI"), and the Individual or Organization
+("Licensee") accessing and otherwise using Python 1.6.1 software in
+source or binary form and its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, CNRI
+hereby grants Licensee a nonexclusive, royalty-free, world-wide
+license to reproduce, analyze, test, perform and/or display publicly,
+prepare derivative works, distribute, and otherwise use Python 1.6.1
+alone or in any derivative version, provided, however, that CNRI's
+License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
+1995-2001 Corporation for National Research Initiatives; All Rights
+Reserved" are retained in Python 1.6.1 alone or in any derivative
+version prepared by Licensee. Alternately, in lieu of CNRI's License
+Agreement, Licensee may substitute the following text (omitting the
+quotes): "Python 1.6.1 is made available subject to the terms and
+conditions in CNRI's License Agreement. This Agreement together with
+Python 1.6.1 may be located on the Internet using the following
+unique, persistent identifier (known as a handle): 1895.22/1013. This
+Agreement may also be obtained from a proxy server on the Internet
+using the following URL: http://hdl.handle.net/1895.22/1013".
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 1.6.1 or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python 1.6.1.
+
+4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
+basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. This License Agreement shall be governed by the federal
+intellectual property law of the United States, including without
+limitation the federal copyright law, and, to the extent such
+U.S. federal law does not apply, by the law of the Commonwealth of
+Virginia, excluding Virginia's conflict of law provisions.
+Notwithstanding the foregoing, with regard to derivative works based
+on Python 1.6.1 that incorporate non-separable material that was
+previously distributed under the GNU General Public License (GPL), the
+law of the Commonwealth of Virginia shall govern this License
+Agreement only as to issues arising under or with respect to
+Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
+License Agreement shall be deemed to create any relationship of
+agency, partnership, or joint venture between CNRI and Licensee. This
+License Agreement does not grant permission to use CNRI trademarks or
+trade name in a trademark sense to endorse or promote products or
+services of Licensee, or any third party.
+
+8. By clicking on the "ACCEPT" button where indicated, or by copying,
+installing or otherwise using Python 1.6.1, Licensee agrees to be
+bound by the terms and conditions of this License Agreement.
+
+ ACCEPT
+
+
+CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
+--------------------------------------------------
+
+Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
+The Netherlands. All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Stichting Mathematisch
+Centrum or CWI not be used in advertising or publicity pertaining to
+distribution of the software without specific, written prior
+permission.
+
+STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
+FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+
******************************
argsarray
@@ -1830,7 +2254,7 @@ argsarray
******************************
array-buffer-byte-length
-1.0.1
+1.0.2
MIT License
Copyright (c) 2023 Inspect JS
@@ -1857,7 +2281,7 @@ SOFTWARE.
******************************
arraybuffer.prototype.slice
-1.0.3
+1.0.4
MIT License
Copyright (c) 2023 ECMAScript Shims
@@ -2583,6 +3007,18 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+******************************
+
+at-least-node
+1.0.0
+The ISC License
+Copyright (c) 2020 Ryan Zimmerman
+
+Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+
******************************
atomic-sleep
@@ -2748,7 +3184,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
@babel/code-frame
-7.24.7
+7.26.2
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -2776,7 +3212,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
@babel/compat-data
-7.24.4
+7.26.5
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -2867,7 +3303,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
@babel/generator
-7.24.7
+7.26.5
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -2895,7 +3331,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
@babel/helper-annotate-as-pure
-7.24.7
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -2923,7 +3359,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
@babel/helper-compilation-targets
-7.23.6
+7.26.5
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -2951,7 +3387,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
@babel/helper-create-class-features-plugin
-7.24.5
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -2979,7 +3415,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
@babel/helper-create-regexp-features-plugin
-7.22.15
+7.26.3
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3062,8 +3498,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-function-name
-7.24.7
+@babel/helper-member-expression-to-functions
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3090,8 +3526,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-hoist-variables
-7.24.7
+@babel/helper-module-imports
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3118,8 +3554,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-member-expression-to-functions
-7.24.5
+@babel/helper-module-transforms
+7.26.0
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3146,8 +3582,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-module-imports
-7.24.7
+@babel/helper-optimise-call-expression
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3174,8 +3610,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-module-transforms
-7.24.5
+@babel/helper-plugin-utils
+7.26.5
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3202,8 +3638,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-optimise-call-expression
-7.22.5
+@babel/helper-remap-async-to-generator
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3230,8 +3666,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-plugin-utils
-7.24.8
+@babel/helper-replace-supers
+7.26.5
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3258,8 +3694,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-remap-async-to-generator
-7.24.7
+@babel/helpers
+7.24.5
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3286,8 +3722,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-replace-supers
-7.24.1
+@babel/helper-skip-transparent-expression-wrappers
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3314,8 +3750,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helpers
-7.24.5
+@babel/helper-string-parser
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3342,8 +3778,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-simple-access
-7.24.5
+@babel/helper-validator-identifier
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3370,8 +3806,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-skip-transparent-expression-wrappers
-7.22.5
+@babel/helper-validator-option
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3398,8 +3834,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-split-export-declaration
-7.24.7
+@babel/helper-wrap-function
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3426,8 +3862,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-string-parser
-7.24.8
+@babel/highlight
+7.24.7
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3454,8 +3890,33 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-validator-identifier
-7.24.7
+@babel/parser
+7.26.5
+Copyright (C) 2012-2014 by various contributors (see AUTHORS)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+******************************
+
+@babel/plugin-bugfix-firefox-class-in-computed-class-key
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3482,8 +3943,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-validator-option
-7.24.8
+@babel/plugin-bugfix-safari-class-field-initializer-scope
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3510,8 +3971,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/helper-wrap-function
-7.24.7
+@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3538,8 +3999,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/highlight
-7.24.7
+@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3566,9 +4027,39 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/parser
-7.24.7
-Copyright (C) 2012-2014 by various contributors (see AUTHORS)
+@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly
+7.25.9
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+******************************
+
+babel-plugin-module-resolver
+5.0.2
+The MIT License (MIT)
+
+Copyright (c) 2015 Tommy Leunen (tommyleunen.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -3620,7 +4111,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
babel-plugin-polyfill-corejs3
-0.10.4
+0.10.6
MIT License
Copyright (c) 2014-present Nicolò Ribaudo and other contributors
@@ -3731,8 +4222,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-proposal-export-default-from
-7.24.1
+@babel/plugin-proposal-decorators
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3759,8 +4250,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-proposal-nullish-coalescing-operator
-7.18.6
+@babel/plugin-proposal-export-default-from
+7.24.1
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3787,8 +4278,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-proposal-numeric-separator
-7.18.6
+@babel/plugin-proposal-export-namespace-from
+7.18.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3815,8 +4306,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-proposal-object-rest-spread
-7.20.7
+@babel/plugin-proposal-nullish-coalescing-operator
+7.18.6
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3843,7 +4334,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-proposal-optional-catch-binding
+@babel/plugin-proposal-numeric-separator
7.18.6
MIT License
@@ -3871,8 +4362,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-proposal-optional-chaining
-7.21.0
+@babel/plugin-proposal-object-rest-spread
+7.20.7
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3899,8 +4390,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-async-generators
-7.8.4
+@babel/plugin-proposal-optional-catch-binding
+7.18.6
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3927,8 +4418,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-class-properties
-7.12.13
+@babel/plugin-proposal-optional-chaining
+7.21.0
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3955,8 +4446,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-dynamic-import
-7.8.3
+@babel/plugin-proposal-private-property-in-object
+7.21.0-placeholder-for-preset-env.2
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -3983,8 +4474,35 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-export-default-from
-7.24.1
+babel-plugin-react-native-web
+0.18.12
+MIT License
+
+Copyright (c) Nicolas Gallagher.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+******************************
+
+@babel/plugin-syntax-async-generators
+7.8.4
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4011,8 +4529,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-flow
-7.24.7
+@babel/plugin-syntax-class-properties
+7.12.13
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4039,8 +4557,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-jsx
-7.24.7
+@babel/plugin-syntax-decorators
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4067,8 +4585,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-nullish-coalescing-operator
-7.8.3
+@babel/plugin-syntax-dynamic-import
+7.8.3
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4095,8 +4613,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-numeric-separator
-7.10.4
+@babel/plugin-syntax-export-default-from
+7.24.1
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4123,8 +4641,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-object-rest-spread
-7.8.3
+@babel/plugin-syntax-export-namespace-from
+7.8.3
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4151,8 +4669,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-optional-catch-binding
-7.8.3
+@babel/plugin-syntax-flow
+7.24.7
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4179,8 +4697,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-syntax-optional-chaining
-7.8.3
+@babel/plugin-syntax-import-assertions
+7.26.0
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4207,15 +4725,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-babel-plugin-syntax-trailing-function-commas
-7.0.0-beta.0
-license: MIT
-authors: undefined
-
-******************************
-
-@babel/plugin-syntax-typescript
-7.24.1
+@babel/plugin-syntax-import-attributes
+7.26.0
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4242,8 +4753,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-arrow-functions
-7.24.1
+@babel/plugin-syntax-jsx
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4270,8 +4781,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-async-to-generator
-7.24.7
+@babel/plugin-syntax-nullish-coalescing-operator
+7.8.3
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4298,8 +4809,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-block-scoped-functions
-7.24.1
+@babel/plugin-syntax-numeric-separator
+7.10.4
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4326,8 +4837,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-block-scoping
-7.24.5
+@babel/plugin-syntax-object-rest-spread
+7.8.3
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4354,8 +4865,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-classes
-7.24.5
+@babel/plugin-syntax-optional-catch-binding
+7.8.3
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4382,8 +4893,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-computed-properties
-7.24.1
+@babel/plugin-syntax-optional-chaining
+7.8.3
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4410,8 +4921,15 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-destructuring
-7.24.7
+babel-plugin-syntax-trailing-function-commas
+7.0.0-beta.0
+license: MIT
+authors: undefined
+
+******************************
+
+@babel/plugin-syntax-typescript
+7.24.1
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4438,35 +4956,36 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-babel-plugin-transform-flow-enums
-0.0.2
+@babel/plugin-syntax-unicode-sets-regex
+7.18.6
MIT License
-Copyright (c) Facebook, Inc. and its affiliates.
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-flow-strip-types
-7.24.7
+@babel/plugin-transform-arrow-functions
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4493,8 +5012,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-for-of
-7.24.1
+@babel/plugin-transform-async-generator-functions
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4521,8 +5040,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-function-name
-7.24.1
+@babel/plugin-transform-async-to-generator
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4549,8 +5068,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-literals
-7.24.1
+@babel/plugin-transform-block-scoped-functions
+7.26.5
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4577,8 +5096,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-member-expression-literals
-7.24.1
+@babel/plugin-transform-block-scoping
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4605,8 +5124,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-modules-commonjs
-7.24.1
+@babel/plugin-transform-classes
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4633,8 +5152,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-named-capturing-groups-regex
-7.22.5
+@babel/plugin-transform-class-properties
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4661,36 +5180,36 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-object-assign
-7.24.1
-The MIT License (MIT)
-
-Copyright (c) 2015 Jed Watson
+@babel/plugin-transform-class-static-block
+7.26.0
+MIT License
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-object-super
-7.24.1
+@babel/plugin-transform-computed-properties
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4717,8 +5236,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-parameters
-7.24.5
+@babel/plugin-transform-destructuring
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4745,8 +5264,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-property-literals
-7.24.1
+@babel/plugin-transform-dotall-regex
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4773,8 +5292,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-react-display-name
-7.24.7
+@babel/plugin-transform-duplicate-keys
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4801,8 +5320,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-react-jsx
-7.25.2
+@babel/plugin-transform-duplicate-named-capturing-groups-regex
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4829,8 +5348,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-react-jsx-self
-7.24.5
+@babel/plugin-transform-dynamic-import
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4857,8 +5376,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-react-jsx-source
-7.24.1
+@babel/plugin-transform-exponentiation-operator
+7.26.3
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4885,8 +5404,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-runtime
-7.24.3
+@babel/plugin-transform-export-namespace-from
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4913,8 +5432,35 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-shorthand-properties
-7.24.1
+babel-plugin-transform-flow-enums
+0.0.2
+MIT License
+
+Copyright (c) Facebook, Inc. and its affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+******************************
+
+@babel/plugin-transform-flow-strip-types
+7.24.7
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4941,8 +5487,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-spread
-7.24.1
+@babel/plugin-transform-for-of
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4969,8 +5515,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-sticky-regex
-7.24.1
+@babel/plugin-transform-function-name
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -4997,8 +5543,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-template-literals
-7.24.1
+@babel/plugin-transform-json-strings
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -5025,8 +5571,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-typescript
-7.24.5
+@babel/plugin-transform-literals
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -5053,8 +5599,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/plugin-transform-unicode-regex
-7.24.1
+@babel/plugin-transform-logical-assignment-operators
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -5081,35 +5627,36 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-babel-preset-fbjs
-3.4.0
+@babel/plugin-transform-member-expression-literals
+7.25.9
MIT License
-Copyright (c) 2013-present, Facebook, Inc.
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/preset-flow
-7.24.1
+@babel/plugin-transform-modules-amd
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -5136,8 +5683,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/preset-typescript
-7.24.1
+@babel/plugin-transform-modules-commonjs
+7.26.3
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -5164,8 +5711,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/register
-7.24.6
+@babel/plugin-transform-modules-systemjs
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -5192,11 +5739,11 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/regjsgen
-0.8.0
-The MIT License (MIT)
+@babel/plugin-transform-modules-umd
+7.25.9
+MIT License
-Copyright 2014-2020 Benjamin Tan
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -5220,8 +5767,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/runtime
-7.24.6
+@babel/plugin-transform-named-capturing-groups-regex
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -5248,8 +5795,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/template
-7.24.7
+@babel/plugin-transform-new-target
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -5276,8 +5823,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/traverse
-7.24.7
+@babel/plugin-transform-nullish-coalescing-operator
+7.26.6
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -5304,8 +5851,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-@babel/types
-7.25.4
+@babel/plugin-transform-numeric-separator
+7.25.9
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
@@ -5332,25 +5879,18 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-backo2
-1.0.2
-license: MIT
-authors: undefined
-
-******************************
-
-balanced-match
-1.0.2
-(MIT)
+@babel/plugin-transform-object-assign
+7.24.1
+The MIT License (MIT)
-Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
+Copyright (c) 2015 Jed Watson
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
@@ -5364,65 +5904,126 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+
******************************
-base58check
-2.0.0
-license: MIT
-authors: unrealce
+@babel/plugin-transform-object-rest-spread
+7.25.9
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
******************************
-base58-js
-1.0.0
+@babel/plugin-transform-object-super
+7.25.9
MIT License
-Copyright (c) 2021 pur3miish
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-Base64
-0.2.1
+@babel/plugin-transform-optional-catch-binding
+7.25.9
+MIT License
- DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
- Version 2, December 2004
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
- Copyright (c) 2011..2012 David Chambers
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
- Everyone is permitted to copy and distribute verbatim or modified
- copies of this license document, and changing it is allowed as long
- as the name is changed.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
- DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- 0. You just DO WHAT THE FUCK YOU WANT TO.
+
+******************************
+
+@babel/plugin-transform-optional-chaining
+7.25.9
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-base-64
-0.1.0
-Copyright Mathias Bynens
+@babel/plugin-transform-parameters
+7.25.9
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -5446,9 +6047,11 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-base-64
-1.0.0
-Copyright Mathias Bynens
+@babel/plugin-transform-private-methods
+7.25.9
+MIT License
+
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -5472,277 +6075,267 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-base64-arraybuffer
-0.1.5
-Copyright (c) 2012 Niklas von Hertzen
+@babel/plugin-transform-private-property-in-object
+7.25.9
+MIT License
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-base64-js
-1.3.1
-The MIT License (MIT)
+@babel/plugin-transform-property-literals
+7.25.9
+MIT License
-Copyright (c) 2014 Jameson Little
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-base64-js
-1.5.1
-The MIT License (MIT)
-
-Copyright (c) 2014 Jameson Little
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+@babel/plugin-transform-react-display-name
+7.24.7
+MIT License
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
-******************************
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-base64-stream
-1.0.0
-license: MIT
-authors: Ross Johnson
******************************
-base-x
-1.1.0
-The MIT License (MIT)
+@babel/plugin-transform-react-jsx
+7.25.9
+MIT License
-Copyright base-x contributors (c) 2016
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-base-x
-3.0.8
-The MIT License (MIT)
+@babel/plugin-transform-react-jsx-self
+7.24.5
+MIT License
-Copyright (c) 2018 base-x contributors
-Copyright (c) 2014-2018 The Bitcoin Core developers
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-basic-ftp
-5.0.3
-Copyright (c) 2019 Patrick Juchli
+@babel/plugin-transform-react-jsx-source
+7.24.1
+MIT License
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
******************************
-bech32
-2.0.0
+@babel/plugin-transform-regenerator
+7.25.9
MIT License
-Copyright (c) 2017 Pieter Wuille
-Copyright (c) 2018 bitcoinjs contributors
+Copyright (c) 2014-present Sebastian McKenzie and other contributors
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************
-big-integer
-1.6.51
-This is free and unencumbered software released into the public domain.
-
-Anyone is free to copy, modify, publish, use, compile, sell, or
-distribute this software, either in source code form or as a compiled
-binary, for any purpose, commercial or non-commercial, and by any
-means.
-
-In jurisdictions that recognize copyright laws, the author or authors
-of this software dedicate any and all copyright interest in the
-software to the public domain. We make this dedication for the benefit
-of the public at large and to the detriment of our heirs and
-successors. We intend this dedication to be an overt act of
-relinquishment in perpetuity of all present and future rights to this
-software under copyright law.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
-ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-
-For more information, please refer to