diff --git a/.eslintrc.js b/.eslintrc.js index 6df1e6e1e52..f8cb2150123 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,5 +39,6 @@ module.exports = { ], 'jest/expect-expect': 'off', 'jest/no-disabled-tests': 'off', + 'no-nested-ternary': 'off', }, }; diff --git a/ios/Images.xcassets/badges/gnosis.imageset/Contents.json b/ios/Images.xcassets/badges/gnosis.imageset/Contents.json new file mode 100644 index 00000000000..834dad265bf --- /dev/null +++ b/ios/Images.xcassets/badges/gnosis.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gnosis.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gnosis@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gnosis@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gnosis.imageset/gnosis.png b/ios/Images.xcassets/badges/gnosis.imageset/gnosis.png new file mode 100644 index 00000000000..afb08cb40e6 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosis.imageset/gnosis.png differ diff --git a/ios/Images.xcassets/badges/gnosis.imageset/gnosis@2x.png b/ios/Images.xcassets/badges/gnosis.imageset/gnosis@2x.png new file mode 100644 index 00000000000..3a021929690 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosis.imageset/gnosis@2x.png differ diff --git a/ios/Images.xcassets/badges/gnosis.imageset/gnosis@3x.png b/ios/Images.xcassets/badges/gnosis.imageset/gnosis@3x.png new file mode 100644 index 00000000000..09946eb88a5 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosis.imageset/gnosis@3x.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadge.imageset/Contents.json b/ios/Images.xcassets/badges/gnosisBadge.imageset/Contents.json new file mode 100644 index 00000000000..a8ae4f3b2ee --- /dev/null +++ b/ios/Images.xcassets/badges/gnosisBadge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gnosisBadge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gnosisBadge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gnosisBadge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gnosisBadge.imageset/gnosisBadge.png b/ios/Images.xcassets/badges/gnosisBadge.imageset/gnosisBadge.png new file mode 100644 index 00000000000..961610867dd Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadge.imageset/gnosisBadge.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadge.imageset/gnosisBadge@2x.png b/ios/Images.xcassets/badges/gnosisBadge.imageset/gnosisBadge@2x.png new file mode 100644 index 00000000000..1355360aca0 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadge.imageset/gnosisBadge@2x.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadge.imageset/gnosisBadge@3x.png b/ios/Images.xcassets/badges/gnosisBadge.imageset/gnosisBadge@3x.png new file mode 100644 index 00000000000..19592fc8d64 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadge.imageset/gnosisBadge@3x.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/Contents.json b/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/Contents.json new file mode 100644 index 00000000000..3e640c6e39e --- /dev/null +++ b/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gnosisBadgeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gnosisBadgeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gnosisBadgeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/gnosisBadgeDark.png b/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/gnosisBadgeDark.png new file mode 100644 index 00000000000..cd7d26152c2 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/gnosisBadgeDark.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/gnosisBadgeDark@2x.png b/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/gnosisBadgeDark@2x.png new file mode 100644 index 00000000000..029c07d9ddb Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/gnosisBadgeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/gnosisBadgeDark@3x.png b/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/gnosisBadgeDark@3x.png new file mode 100644 index 00000000000..5c8ed229375 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeDark.imageset/gnosisBadgeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/Contents.json b/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/Contents.json new file mode 100644 index 00000000000..5f89716e5e9 --- /dev/null +++ b/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gnosisBadgeLarge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gnosisBadgeLarge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gnosisBadgeLarge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/gnosisBadgeLarge.png b/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/gnosisBadgeLarge.png new file mode 100644 index 00000000000..9cf5511d8f1 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/gnosisBadgeLarge.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/gnosisBadgeLarge@2x.png b/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/gnosisBadgeLarge@2x.png new file mode 100644 index 00000000000..301ddfb2914 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/gnosisBadgeLarge@2x.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/gnosisBadgeLarge@3x.png b/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/gnosisBadgeLarge@3x.png new file mode 100644 index 00000000000..e787754b713 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeLarge.imageset/gnosisBadgeLarge@3x.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/Contents.json b/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/Contents.json new file mode 100644 index 00000000000..ee423b90122 --- /dev/null +++ b/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gnosisBadgeLargeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gnosisBadgeLargeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gnosisBadgeLargeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/gnosisBadgeLargeDark.png b/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/gnosisBadgeLargeDark.png new file mode 100644 index 00000000000..4f3b0c859cb Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/gnosisBadgeLargeDark.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/gnosisBadgeLargeDark@2x.png b/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/gnosisBadgeLargeDark@2x.png new file mode 100644 index 00000000000..79768e5d97e Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/gnosisBadgeLargeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/gnosisBadgeLargeDark@3x.png b/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/gnosisBadgeLargeDark@3x.png new file mode 100644 index 00000000000..4975e795ef0 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeLargeDark.imageset/gnosisBadgeLargeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/Contents.json b/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/Contents.json new file mode 100644 index 00000000000..f322a845147 --- /dev/null +++ b/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gnosisBadgeNoShadow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gnosisBadgeNoShadow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gnosisBadgeNoShadow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/gnosisBadgeNoShadow.png b/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/gnosisBadgeNoShadow.png new file mode 100644 index 00000000000..baa5ee94397 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/gnosisBadgeNoShadow.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/gnosisBadgeNoShadow@2x.png b/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/gnosisBadgeNoShadow@2x.png new file mode 100644 index 00000000000..57e1e098b6a Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/gnosisBadgeNoShadow@2x.png differ diff --git a/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/gnosisBadgeNoShadow@3x.png b/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/gnosisBadgeNoShadow@3x.png new file mode 100644 index 00000000000..42a52730e27 Binary files /dev/null and b/ios/Images.xcassets/badges/gnosisBadgeNoShadow.imageset/gnosisBadgeNoShadow@3x.png differ diff --git a/ios/Images.xcassets/badges/gravity.imageset/Contents.json b/ios/Images.xcassets/badges/gravity.imageset/Contents.json new file mode 100644 index 00000000000..5e1cbb0a037 --- /dev/null +++ b/ios/Images.xcassets/badges/gravity.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gravity.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gravity@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gravity@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gravity.imageset/gravity.png b/ios/Images.xcassets/badges/gravity.imageset/gravity.png new file mode 100644 index 00000000000..a515d5551c5 Binary files /dev/null and b/ios/Images.xcassets/badges/gravity.imageset/gravity.png differ diff --git a/ios/Images.xcassets/badges/gravity.imageset/gravity@2x.png b/ios/Images.xcassets/badges/gravity.imageset/gravity@2x.png new file mode 100644 index 00000000000..29e60cbad1c Binary files /dev/null and b/ios/Images.xcassets/badges/gravity.imageset/gravity@2x.png differ diff --git a/ios/Images.xcassets/badges/gravity.imageset/gravity@3x.png b/ios/Images.xcassets/badges/gravity.imageset/gravity@3x.png new file mode 100644 index 00000000000..55108967dea Binary files /dev/null and b/ios/Images.xcassets/badges/gravity.imageset/gravity@3x.png differ diff --git a/ios/Images.xcassets/badges/gravityBadge.imageset/Contents.json b/ios/Images.xcassets/badges/gravityBadge.imageset/Contents.json new file mode 100644 index 00000000000..83f2a19de8d --- /dev/null +++ b/ios/Images.xcassets/badges/gravityBadge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gravityBadge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gravityBadge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gravityBadge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gravityBadge.imageset/gravityBadge.png b/ios/Images.xcassets/badges/gravityBadge.imageset/gravityBadge.png new file mode 100644 index 00000000000..ad033ad3691 Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadge.imageset/gravityBadge.png differ diff --git a/ios/Images.xcassets/badges/gravityBadge.imageset/gravityBadge@2x.png b/ios/Images.xcassets/badges/gravityBadge.imageset/gravityBadge@2x.png new file mode 100644 index 00000000000..2264da68ec9 Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadge.imageset/gravityBadge@2x.png differ diff --git a/ios/Images.xcassets/badges/gravityBadge.imageset/gravityBadge@3x.png b/ios/Images.xcassets/badges/gravityBadge.imageset/gravityBadge@3x.png new file mode 100644 index 00000000000..8d6bdddc381 Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadge.imageset/gravityBadge@3x.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeDark.imageset/Contents.json b/ios/Images.xcassets/badges/gravityBadgeDark.imageset/Contents.json new file mode 100644 index 00000000000..652b4b30a25 --- /dev/null +++ b/ios/Images.xcassets/badges/gravityBadgeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gravityBadgeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gravityBadgeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gravityBadgeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gravityBadgeDark.imageset/gravityBadgeDark.png b/ios/Images.xcassets/badges/gravityBadgeDark.imageset/gravityBadgeDark.png new file mode 100644 index 00000000000..02fb9e0677f Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeDark.imageset/gravityBadgeDark.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeDark.imageset/gravityBadgeDark@2x.png b/ios/Images.xcassets/badges/gravityBadgeDark.imageset/gravityBadgeDark@2x.png new file mode 100644 index 00000000000..9aadd9549ae Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeDark.imageset/gravityBadgeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeDark.imageset/gravityBadgeDark@3x.png b/ios/Images.xcassets/badges/gravityBadgeDark.imageset/gravityBadgeDark@3x.png new file mode 100644 index 00000000000..5218bf5b88e Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeDark.imageset/gravityBadgeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/Contents.json b/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/Contents.json new file mode 100644 index 00000000000..1a4bb0120b1 --- /dev/null +++ b/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gravityBadgeLarge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gravityBadgeLarge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gravityBadgeLarge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/gravityBadgeLarge.png b/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/gravityBadgeLarge.png new file mode 100644 index 00000000000..ccfed23d64b Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/gravityBadgeLarge.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/gravityBadgeLarge@2x.png b/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/gravityBadgeLarge@2x.png new file mode 100644 index 00000000000..51e00178021 Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/gravityBadgeLarge@2x.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/gravityBadgeLarge@3x.png b/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/gravityBadgeLarge@3x.png new file mode 100644 index 00000000000..805d9fcd7b9 Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeLarge.imageset/gravityBadgeLarge@3x.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/Contents.json b/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/Contents.json new file mode 100644 index 00000000000..0d735b28d3a --- /dev/null +++ b/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gravityBadgeLargeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gravityBadgeLargeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gravityBadgeLargeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/gravityBadgeLargeDark.png b/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/gravityBadgeLargeDark.png new file mode 100644 index 00000000000..ab9ee2fcbae Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/gravityBadgeLargeDark.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/gravityBadgeLargeDark@2x.png b/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/gravityBadgeLargeDark@2x.png new file mode 100644 index 00000000000..067b373a7e4 Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/gravityBadgeLargeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/gravityBadgeLargeDark@3x.png b/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/gravityBadgeLargeDark@3x.png new file mode 100644 index 00000000000..362d6f3d772 Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeLargeDark.imageset/gravityBadgeLargeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/Contents.json b/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/Contents.json new file mode 100644 index 00000000000..33f754ed1be --- /dev/null +++ b/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gravityBadgeNoShadow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gravityBadgeNoShadow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gravityBadgeNoShadow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/gravityBadgeNoShadow.png b/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/gravityBadgeNoShadow.png new file mode 100644 index 00000000000..9492a525822 Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/gravityBadgeNoShadow.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/gravityBadgeNoShadow@2x.png b/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/gravityBadgeNoShadow@2x.png new file mode 100644 index 00000000000..5052d829f1e Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/gravityBadgeNoShadow@2x.png differ diff --git a/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/gravityBadgeNoShadow@3x.png b/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/gravityBadgeNoShadow@3x.png new file mode 100644 index 00000000000..474f948bf99 Binary files /dev/null and b/ios/Images.xcassets/badges/gravityBadgeNoShadow.imageset/gravityBadgeNoShadow@3x.png differ diff --git a/ios/Images.xcassets/badges/linea.imageset/Contents.json b/ios/Images.xcassets/badges/linea.imageset/Contents.json new file mode 100644 index 00000000000..673ca809472 --- /dev/null +++ b/ios/Images.xcassets/badges/linea.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "linea.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "linea@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "linea@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/linea.imageset/linea.png b/ios/Images.xcassets/badges/linea.imageset/linea.png new file mode 100644 index 00000000000..14e4a7fface Binary files /dev/null and b/ios/Images.xcassets/badges/linea.imageset/linea.png differ diff --git a/ios/Images.xcassets/badges/linea.imageset/linea@2x.png b/ios/Images.xcassets/badges/linea.imageset/linea@2x.png new file mode 100644 index 00000000000..42f41b35f9d Binary files /dev/null and b/ios/Images.xcassets/badges/linea.imageset/linea@2x.png differ diff --git a/ios/Images.xcassets/badges/linea.imageset/linea@3x.png b/ios/Images.xcassets/badges/linea.imageset/linea@3x.png new file mode 100644 index 00000000000..4cc7abd0cdc Binary files /dev/null and b/ios/Images.xcassets/badges/linea.imageset/linea@3x.png differ diff --git a/ios/Images.xcassets/badges/lineaBadge.imageset/Contents.json b/ios/Images.xcassets/badges/lineaBadge.imageset/Contents.json new file mode 100644 index 00000000000..01ecb7425f8 --- /dev/null +++ b/ios/Images.xcassets/badges/lineaBadge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "lineaBadge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "lineaBadge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "lineaBadge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/lineaBadge.imageset/lineaBadge.png b/ios/Images.xcassets/badges/lineaBadge.imageset/lineaBadge.png new file mode 100644 index 00000000000..267723587ca Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadge.imageset/lineaBadge.png differ diff --git a/ios/Images.xcassets/badges/lineaBadge.imageset/lineaBadge@2x.png b/ios/Images.xcassets/badges/lineaBadge.imageset/lineaBadge@2x.png new file mode 100644 index 00000000000..70bb69fae24 Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadge.imageset/lineaBadge@2x.png differ diff --git a/ios/Images.xcassets/badges/lineaBadge.imageset/lineaBadge@3x.png b/ios/Images.xcassets/badges/lineaBadge.imageset/lineaBadge@3x.png new file mode 100644 index 00000000000..814892f7fdb Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadge.imageset/lineaBadge@3x.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeDark.imageset/Contents.json b/ios/Images.xcassets/badges/lineaBadgeDark.imageset/Contents.json new file mode 100644 index 00000000000..18b85aa99d7 --- /dev/null +++ b/ios/Images.xcassets/badges/lineaBadgeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "lineaBadgeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "lineaBadgeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "lineaBadgeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/lineaBadgeDark.imageset/lineaBadgeDark.png b/ios/Images.xcassets/badges/lineaBadgeDark.imageset/lineaBadgeDark.png new file mode 100644 index 00000000000..1fa46075d00 Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeDark.imageset/lineaBadgeDark.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeDark.imageset/lineaBadgeDark@2x.png b/ios/Images.xcassets/badges/lineaBadgeDark.imageset/lineaBadgeDark@2x.png new file mode 100644 index 00000000000..152eb8d6177 Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeDark.imageset/lineaBadgeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeDark.imageset/lineaBadgeDark@3x.png b/ios/Images.xcassets/badges/lineaBadgeDark.imageset/lineaBadgeDark@3x.png new file mode 100644 index 00000000000..a58ff5b93ac Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeDark.imageset/lineaBadgeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/Contents.json b/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/Contents.json new file mode 100644 index 00000000000..ed3cb5016ee --- /dev/null +++ b/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "lineaBadgeLarge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "lineaBadgeLarge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "lineaBadgeLarge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/lineaBadgeLarge.png b/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/lineaBadgeLarge.png new file mode 100644 index 00000000000..93a23a92347 Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/lineaBadgeLarge.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/lineaBadgeLarge@2x.png b/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/lineaBadgeLarge@2x.png new file mode 100644 index 00000000000..0fe5d25acdf Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/lineaBadgeLarge@2x.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/lineaBadgeLarge@3x.png b/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/lineaBadgeLarge@3x.png new file mode 100644 index 00000000000..4f3a3c0ce1c Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeLarge.imageset/lineaBadgeLarge@3x.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/Contents.json b/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/Contents.json new file mode 100644 index 00000000000..0a56a80d868 --- /dev/null +++ b/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "lineaBadgeLargeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "lineaBadgeLargeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "lineaBadgeLargeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/lineaBadgeLargeDark.png b/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/lineaBadgeLargeDark.png new file mode 100644 index 00000000000..f463c5abcc7 Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/lineaBadgeLargeDark.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/lineaBadgeLargeDark@2x.png b/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/lineaBadgeLargeDark@2x.png new file mode 100644 index 00000000000..f417f8f1f16 Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/lineaBadgeLargeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/lineaBadgeLargeDark@3x.png b/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/lineaBadgeLargeDark@3x.png new file mode 100644 index 00000000000..86c51a9e20b Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeLargeDark.imageset/lineaBadgeLargeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/Contents.json b/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/Contents.json new file mode 100644 index 00000000000..1e5565d1c2c --- /dev/null +++ b/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "lineaBadgeNoShadow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "lineaBadgeNoShadow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "lineaBadgeNoShadow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/lineaBadgeNoShadow.png b/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/lineaBadgeNoShadow.png new file mode 100644 index 00000000000..1af3c5fd42d Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/lineaBadgeNoShadow.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/lineaBadgeNoShadow@2x.png b/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/lineaBadgeNoShadow@2x.png new file mode 100644 index 00000000000..cddfa99c67f Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/lineaBadgeNoShadow@2x.png differ diff --git a/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/lineaBadgeNoShadow@3x.png b/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/lineaBadgeNoShadow@3x.png new file mode 100644 index 00000000000..22007385103 Binary files /dev/null and b/ios/Images.xcassets/badges/lineaBadgeNoShadow.imageset/lineaBadgeNoShadow@3x.png differ diff --git a/ios/Images.xcassets/badges/sankBadgeDark.imageset/Contents.json b/ios/Images.xcassets/badges/sankBadgeDark.imageset/Contents.json new file mode 100644 index 00000000000..da70d3d22ef --- /dev/null +++ b/ios/Images.xcassets/badges/sankBadgeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sankBadgeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sankBadgeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sankBadgeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/sankBadgeDark.imageset/sankBadgeDark.png b/ios/Images.xcassets/badges/sankBadgeDark.imageset/sankBadgeDark.png new file mode 100644 index 00000000000..6ab341257ab Binary files /dev/null and b/ios/Images.xcassets/badges/sankBadgeDark.imageset/sankBadgeDark.png differ diff --git a/ios/Images.xcassets/badges/sankBadgeDark.imageset/sankBadgeDark@2x.png b/ios/Images.xcassets/badges/sankBadgeDark.imageset/sankBadgeDark@2x.png new file mode 100644 index 00000000000..d4e5beded3f Binary files /dev/null and b/ios/Images.xcassets/badges/sankBadgeDark.imageset/sankBadgeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/sankBadgeDark.imageset/sankBadgeDark@3x.png b/ios/Images.xcassets/badges/sankBadgeDark.imageset/sankBadgeDark@3x.png new file mode 100644 index 00000000000..9b5eba8fe7a Binary files /dev/null and b/ios/Images.xcassets/badges/sankBadgeDark.imageset/sankBadgeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/sanko.imageset/Contents.json b/ios/Images.xcassets/badges/sanko.imageset/Contents.json new file mode 100644 index 00000000000..ba52a5b3aeb --- /dev/null +++ b/ios/Images.xcassets/badges/sanko.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sanko.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sanko@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sanko@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/sanko.imageset/sanko.png b/ios/Images.xcassets/badges/sanko.imageset/sanko.png new file mode 100644 index 00000000000..e0eff636cac Binary files /dev/null and b/ios/Images.xcassets/badges/sanko.imageset/sanko.png differ diff --git a/ios/Images.xcassets/badges/sanko.imageset/sanko@2x.png b/ios/Images.xcassets/badges/sanko.imageset/sanko@2x.png new file mode 100644 index 00000000000..399f9ad13e6 Binary files /dev/null and b/ios/Images.xcassets/badges/sanko.imageset/sanko@2x.png differ diff --git a/ios/Images.xcassets/badges/sanko.imageset/sanko@3x.png b/ios/Images.xcassets/badges/sanko.imageset/sanko@3x.png new file mode 100644 index 00000000000..c5c5e42e575 Binary files /dev/null and b/ios/Images.xcassets/badges/sanko.imageset/sanko@3x.png differ diff --git a/ios/Images.xcassets/badges/sankoBadge.imageset/Contents.json b/ios/Images.xcassets/badges/sankoBadge.imageset/Contents.json new file mode 100644 index 00000000000..e34f3e90e70 --- /dev/null +++ b/ios/Images.xcassets/badges/sankoBadge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sankoBadge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sankoBadge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sankoBadge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/sankoBadge.imageset/sankoBadge.png b/ios/Images.xcassets/badges/sankoBadge.imageset/sankoBadge.png new file mode 100644 index 00000000000..3e44d659f3a Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadge.imageset/sankoBadge.png differ diff --git a/ios/Images.xcassets/badges/sankoBadge.imageset/sankoBadge@2x.png b/ios/Images.xcassets/badges/sankoBadge.imageset/sankoBadge@2x.png new file mode 100644 index 00000000000..39a74fcdc8d Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadge.imageset/sankoBadge@2x.png differ diff --git a/ios/Images.xcassets/badges/sankoBadge.imageset/sankoBadge@3x.png b/ios/Images.xcassets/badges/sankoBadge.imageset/sankoBadge@3x.png new file mode 100644 index 00000000000..a9729e07af3 Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadge.imageset/sankoBadge@3x.png differ diff --git a/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/Contents.json b/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/Contents.json new file mode 100644 index 00000000000..8d3d05e697d --- /dev/null +++ b/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sankoBadgeLarge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sankoBadgeLarge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sankoBadgeLarge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/sankoBadgeLarge.png b/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/sankoBadgeLarge.png new file mode 100644 index 00000000000..e2ead662090 Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/sankoBadgeLarge.png differ diff --git a/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/sankoBadgeLarge@2x.png b/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/sankoBadgeLarge@2x.png new file mode 100644 index 00000000000..726dfc00978 Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/sankoBadgeLarge@2x.png differ diff --git a/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/sankoBadgeLarge@3x.png b/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/sankoBadgeLarge@3x.png new file mode 100644 index 00000000000..897a700db31 Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadgeLarge.imageset/sankoBadgeLarge@3x.png differ diff --git a/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/Contents.json b/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/Contents.json new file mode 100644 index 00000000000..969adc60502 --- /dev/null +++ b/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sankoBadgeLargeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sankoBadgeLargeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sankoBadgeLargeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/sankoBadgeLargeDark.png b/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/sankoBadgeLargeDark.png new file mode 100644 index 00000000000..0218596d5bb Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/sankoBadgeLargeDark.png differ diff --git a/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/sankoBadgeLargeDark@2x.png b/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/sankoBadgeLargeDark@2x.png new file mode 100644 index 00000000000..40c50ffecf6 Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/sankoBadgeLargeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/sankoBadgeLargeDark@3x.png b/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/sankoBadgeLargeDark@3x.png new file mode 100644 index 00000000000..f683ef22d4b Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadgeLargeDark.imageset/sankoBadgeLargeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/Contents.json b/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/Contents.json new file mode 100644 index 00000000000..3b1ea006654 --- /dev/null +++ b/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sankoBadgeNoShadow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sankoBadgeNoShadow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sankoBadgeNoShadow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/sankoBadgeNoShadow.png b/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/sankoBadgeNoShadow.png new file mode 100644 index 00000000000..b0b2c0385ac Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/sankoBadgeNoShadow.png differ diff --git a/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/sankoBadgeNoShadow@2x.png b/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/sankoBadgeNoShadow@2x.png new file mode 100644 index 00000000000..990e00a88b0 Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/sankoBadgeNoShadow@2x.png differ diff --git a/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/sankoBadgeNoShadow@3x.png b/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/sankoBadgeNoShadow@3x.png new file mode 100644 index 00000000000..f79b957c903 Binary files /dev/null and b/ios/Images.xcassets/badges/sankoBadgeNoShadow.imageset/sankoBadgeNoShadow@3x.png differ diff --git a/ios/Images.xcassets/badges/scroll.imageset/Contents.json b/ios/Images.xcassets/badges/scroll.imageset/Contents.json new file mode 100644 index 00000000000..2496dd78ca3 --- /dev/null +++ b/ios/Images.xcassets/badges/scroll.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "scroll.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "scroll@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "scroll@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/scroll.imageset/scroll.png b/ios/Images.xcassets/badges/scroll.imageset/scroll.png new file mode 100644 index 00000000000..b074e4e54f3 Binary files /dev/null and b/ios/Images.xcassets/badges/scroll.imageset/scroll.png differ diff --git a/ios/Images.xcassets/badges/scroll.imageset/scroll@2x.png b/ios/Images.xcassets/badges/scroll.imageset/scroll@2x.png new file mode 100644 index 00000000000..1dab422416c Binary files /dev/null and b/ios/Images.xcassets/badges/scroll.imageset/scroll@2x.png differ diff --git a/ios/Images.xcassets/badges/scroll.imageset/scroll@3x.png b/ios/Images.xcassets/badges/scroll.imageset/scroll@3x.png new file mode 100644 index 00000000000..04482628c63 Binary files /dev/null and b/ios/Images.xcassets/badges/scroll.imageset/scroll@3x.png differ diff --git a/ios/Images.xcassets/badges/scrollBadge.imageset/Contents.json b/ios/Images.xcassets/badges/scrollBadge.imageset/Contents.json new file mode 100644 index 00000000000..764ef1d584d --- /dev/null +++ b/ios/Images.xcassets/badges/scrollBadge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "scrollBadge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "scrollBadge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "scrollBadge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/scrollBadge.imageset/scrollBadge.png b/ios/Images.xcassets/badges/scrollBadge.imageset/scrollBadge.png new file mode 100644 index 00000000000..3266ab33b57 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadge.imageset/scrollBadge.png differ diff --git a/ios/Images.xcassets/badges/scrollBadge.imageset/scrollBadge@2x.png b/ios/Images.xcassets/badges/scrollBadge.imageset/scrollBadge@2x.png new file mode 100644 index 00000000000..aea2e050f18 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadge.imageset/scrollBadge@2x.png differ diff --git a/ios/Images.xcassets/badges/scrollBadge.imageset/scrollBadge@3x.png b/ios/Images.xcassets/badges/scrollBadge.imageset/scrollBadge@3x.png new file mode 100644 index 00000000000..40f77762f36 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadge.imageset/scrollBadge@3x.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeDark.imageset/Contents.json b/ios/Images.xcassets/badges/scrollBadgeDark.imageset/Contents.json new file mode 100644 index 00000000000..a11922fe560 --- /dev/null +++ b/ios/Images.xcassets/badges/scrollBadgeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "scrollBadgeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "scrollBadgeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "scrollBadgeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/scrollBadgeDark.imageset/scrollBadgeDark.png b/ios/Images.xcassets/badges/scrollBadgeDark.imageset/scrollBadgeDark.png new file mode 100644 index 00000000000..71927e891e8 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeDark.imageset/scrollBadgeDark.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeDark.imageset/scrollBadgeDark@2x.png b/ios/Images.xcassets/badges/scrollBadgeDark.imageset/scrollBadgeDark@2x.png new file mode 100644 index 00000000000..53095e21774 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeDark.imageset/scrollBadgeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeDark.imageset/scrollBadgeDark@3x.png b/ios/Images.xcassets/badges/scrollBadgeDark.imageset/scrollBadgeDark@3x.png new file mode 100644 index 00000000000..5bfd03fdd2b Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeDark.imageset/scrollBadgeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/Contents.json b/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/Contents.json new file mode 100644 index 00000000000..94fc4d4b6e8 --- /dev/null +++ b/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "scrollBadgeLarge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "scrollBadgeLarge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "scrollBadgeLarge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/scrollBadgeLarge.png b/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/scrollBadgeLarge.png new file mode 100644 index 00000000000..cc1dbc31e63 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/scrollBadgeLarge.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/scrollBadgeLarge@2x.png b/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/scrollBadgeLarge@2x.png new file mode 100644 index 00000000000..c60eca2ff99 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/scrollBadgeLarge@2x.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/scrollBadgeLarge@3x.png b/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/scrollBadgeLarge@3x.png new file mode 100644 index 00000000000..8be695f8247 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeLarge.imageset/scrollBadgeLarge@3x.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/Contents.json b/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/Contents.json new file mode 100644 index 00000000000..e6b3dcb2f44 --- /dev/null +++ b/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "scrollBadgeLargeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "scrollBadgeLargeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "scrollBadgeLargeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/scrollBadgeLargeDark.png b/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/scrollBadgeLargeDark.png new file mode 100644 index 00000000000..5ecf4f2dff7 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/scrollBadgeLargeDark.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/scrollBadgeLargeDark@2x.png b/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/scrollBadgeLargeDark@2x.png new file mode 100644 index 00000000000..5c6b5d6a50c Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/scrollBadgeLargeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/scrollBadgeLargeDark@3x.png b/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/scrollBadgeLargeDark@3x.png new file mode 100644 index 00000000000..0e508374d35 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeLargeDark.imageset/scrollBadgeLargeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/Contents.json b/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/Contents.json new file mode 100644 index 00000000000..6a6f34f28ca --- /dev/null +++ b/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "scrollBadgeNoShadow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "scrollBadgeNoShadow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "scrollBadgeNoShadow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/scrollBadgeNoShadow.png b/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/scrollBadgeNoShadow.png new file mode 100644 index 00000000000..cfdef2d77c9 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/scrollBadgeNoShadow.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/scrollBadgeNoShadow@2x.png b/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/scrollBadgeNoShadow@2x.png new file mode 100644 index 00000000000..79c1aa6e3b0 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/scrollBadgeNoShadow@2x.png differ diff --git a/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/scrollBadgeNoShadow@3x.png b/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/scrollBadgeNoShadow@3x.png new file mode 100644 index 00000000000..d140debde92 Binary files /dev/null and b/ios/Images.xcassets/badges/scrollBadgeNoShadow.imageset/scrollBadgeNoShadow@3x.png differ diff --git a/ios/Images.xcassets/badges/zkSyncBadge.imageset/Contents.json b/ios/Images.xcassets/badges/zkSyncBadge.imageset/Contents.json new file mode 100644 index 00000000000..0a103fcddc5 --- /dev/null +++ b/ios/Images.xcassets/badges/zkSyncBadge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "zkSyncBadge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "zkSyncBadge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "zkSyncBadge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/zkSyncBadge.imageset/zkSyncBadge.png b/ios/Images.xcassets/badges/zkSyncBadge.imageset/zkSyncBadge.png new file mode 100644 index 00000000000..f84a89c233f Binary files /dev/null and b/ios/Images.xcassets/badges/zkSyncBadge.imageset/zkSyncBadge.png differ diff --git a/ios/Images.xcassets/badges/zkSyncBadge.imageset/zkSyncBadge@2x.png b/ios/Images.xcassets/badges/zkSyncBadge.imageset/zkSyncBadge@2x.png new file mode 100644 index 00000000000..02a0f753727 Binary files /dev/null and b/ios/Images.xcassets/badges/zkSyncBadge.imageset/zkSyncBadge@2x.png differ diff --git a/ios/Images.xcassets/badges/zkSyncBadge.imageset/zkSyncBadge@3x.png b/ios/Images.xcassets/badges/zkSyncBadge.imageset/zkSyncBadge@3x.png new file mode 100644 index 00000000000..db578e63d79 Binary files /dev/null and b/ios/Images.xcassets/badges/zkSyncBadge.imageset/zkSyncBadge@3x.png differ diff --git a/ios/Images.xcassets/badges/zksync.imageset/Contents.json b/ios/Images.xcassets/badges/zksync.imageset/Contents.json new file mode 100644 index 00000000000..c6ca19bfaf8 --- /dev/null +++ b/ios/Images.xcassets/badges/zksync.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "zksync.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "zksync@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "zksync@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/zksync.imageset/zksync.png b/ios/Images.xcassets/badges/zksync.imageset/zksync.png new file mode 100644 index 00000000000..1225b4e6c6e Binary files /dev/null and b/ios/Images.xcassets/badges/zksync.imageset/zksync.png differ diff --git a/ios/Images.xcassets/badges/zksync.imageset/zksync@2x.png b/ios/Images.xcassets/badges/zksync.imageset/zksync@2x.png new file mode 100644 index 00000000000..b437d430a93 Binary files /dev/null and b/ios/Images.xcassets/badges/zksync.imageset/zksync@2x.png differ diff --git a/ios/Images.xcassets/badges/zksync.imageset/zksync@3x.png b/ios/Images.xcassets/badges/zksync.imageset/zksync@3x.png new file mode 100644 index 00000000000..2e433247e12 Binary files /dev/null and b/ios/Images.xcassets/badges/zksync.imageset/zksync@3x.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/Contents.json b/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/Contents.json new file mode 100644 index 00000000000..c595c48304b --- /dev/null +++ b/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "zksyncBadgeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "zksyncBadgeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "zksyncBadgeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/zksyncBadgeDark.png b/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/zksyncBadgeDark.png new file mode 100644 index 00000000000..b6947dbcd88 Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/zksyncBadgeDark.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/zksyncBadgeDark@2x.png b/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/zksyncBadgeDark@2x.png new file mode 100644 index 00000000000..77f075eb22e Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/zksyncBadgeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/zksyncBadgeDark@3x.png b/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/zksyncBadgeDark@3x.png new file mode 100644 index 00000000000..a7ea118d5ce Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeDark.imageset/zksyncBadgeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/Contents.json b/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/Contents.json new file mode 100644 index 00000000000..8df41d41eeb --- /dev/null +++ b/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "zksyncBadgeLarge.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "zksyncBadgeLarge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "zksyncBadgeLarge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/zksyncBadgeLarge.png b/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/zksyncBadgeLarge.png new file mode 100644 index 00000000000..02190afd30f Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/zksyncBadgeLarge.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/zksyncBadgeLarge@2x.png b/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/zksyncBadgeLarge@2x.png new file mode 100644 index 00000000000..420b0f8531e Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/zksyncBadgeLarge@2x.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/zksyncBadgeLarge@3x.png b/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/zksyncBadgeLarge@3x.png new file mode 100644 index 00000000000..25aef546ecf Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeLarge.imageset/zksyncBadgeLarge@3x.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/Contents.json b/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/Contents.json new file mode 100644 index 00000000000..aae27eaf7dc --- /dev/null +++ b/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "zksyncBadgeLargeDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "zksyncBadgeLargeDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "zksyncBadgeLargeDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/zksyncBadgeLargeDark.png b/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/zksyncBadgeLargeDark.png new file mode 100644 index 00000000000..19a58296e9a Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/zksyncBadgeLargeDark.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/zksyncBadgeLargeDark@2x.png b/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/zksyncBadgeLargeDark@2x.png new file mode 100644 index 00000000000..ea8d6389607 Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/zksyncBadgeLargeDark@2x.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/zksyncBadgeLargeDark@3x.png b/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/zksyncBadgeLargeDark@3x.png new file mode 100644 index 00000000000..71a30293a2e Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeLargeDark.imageset/zksyncBadgeLargeDark@3x.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/Contents.json b/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/Contents.json new file mode 100644 index 00000000000..caf3f48aeb6 --- /dev/null +++ b/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "zksyncBadgeNoShadow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "zksyncBadgeNoShadow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "zksyncBadgeNoShadow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/zksyncBadgeNoShadow.png b/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/zksyncBadgeNoShadow.png new file mode 100644 index 00000000000..47b5500b5d0 Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/zksyncBadgeNoShadow.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/zksyncBadgeNoShadow@2x.png b/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/zksyncBadgeNoShadow@2x.png new file mode 100644 index 00000000000..97f1bc72954 Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/zksyncBadgeNoShadow@2x.png differ diff --git a/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/zksyncBadgeNoShadow@3x.png b/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/zksyncBadgeNoShadow@3x.png new file mode 100644 index 00000000000..ed087b5f411 Binary files /dev/null and b/ios/Images.xcassets/badges/zksyncBadgeNoShadow.imageset/zksyncBadgeNoShadow@3x.png differ diff --git a/scripts/add_network.sh b/scripts/add_network.sh deleted file mode 100755 index 6750607038f..00000000000 --- a/scripts/add_network.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash - -# Prompt for network details -read -p "Enter the network name (case sensitive): " networkName -read -p "Enter the chain ID (number): " chainId -read -p "Enter the light mode color (hex): " lightColor -read -p "Enter the dark mode color (hex): " darkColor - -# Create imagesets -mkdir -p "ios/Images.xcassets/badges/${networkName}.imageset" -mkdir -p "ios/Images.xcassets/badges/${networkName}Badge.imageset" -mkdir -p "ios/Images.xcassets/badges/${networkName}BadgeDark.imageset" -mkdir -p "ios/Images.xcassets/badges/${networkName}BadgeLarge.imageset" -mkdir -p "ios/Images.xcassets/badges/${networkName}BadgeLargeDark.imageset" -mkdir -p "ios/Images.xcassets/badges/${networkName}BadgeNoShadow.imageset" - -# Create Contents.json for each imageset -for suffix in "" "Badge" "BadgeDark" "BadgeLarge" "BadgeLargeDark" "BadgeNoShadow"; do - cat > "ios/Images.xcassets/badges/${networkName}${suffix}.imageset/Contents.json" << EOF -{ - "images" : [ - { - "filename" : "${networkName}${suffix}.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "${networkName}${suffix}@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "${networkName}${suffix}@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} -EOF -done - -# Update en_US.json -# Using perl for more precise JSON manipulation -perl -i -0pe 's/("explain":\s*{)/$1\n "'$networkName'": {\n "text": "",\n "title": "What'\''s '$networkName'?"\n },/m' src/languages/en_US.json - -# Update types.ts -# Add to Network enum -sed -i '' "/export enum Network {/a\\ - ${networkName} = '${networkName}', -" src/chains/types.ts - -# Add to ChainId enum -sed -i '' "/export enum ChainId {/a\\ - ${networkName} = ${chainId}, -" src/chains/types.ts - -# Update colors.ts for light mode - look for the first networkColors declaration -sed -i '' "/^ let networkColors = {/a\\ - [ChainId.${networkName}]: '${lightColor}', -" src/styles/colors.ts - -# Update colors.ts for dark mode - look specifically in the darkMode if block -sed -i '' "/if (darkMode) {/,/^ }/ { - /networkColors = {/a\\ - [ChainId.${networkName}]: '${darkColor}', -}" src/styles/colors.ts - -echo "Network ${networkName} has been added!" -echo "Note: You'll need to add the actual badge images to the imageset directories" -echo "Don't forget to run prettier to format the modified files" \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 157ff68b606..1a749b8ce6d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import '@/languages'; import * as Sentry from '@sentry/react-native'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, memo } from 'react'; import { AppRegistry, Dimensions, LogBox, StyleSheet, View } from 'react-native'; import { Toaster } from 'sonner-native'; import { MobileWalletProtocolProvider } from '@coinbase/mobile-wallet-protocol-host'; @@ -9,9 +9,8 @@ import { useApplicationSetup } from '@/hooks/useApplicationSetup'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'; import { enableScreens } from 'react-native-screens'; -import { connect, Provider as ReduxProvider } from 'react-redux'; +import { connect, Provider as ReduxProvider, shallowEqual } from 'react-redux'; import { RecoilRoot } from 'recoil'; -import PortalConsumer from '@/components/PortalConsumer'; import ErrorBoundary from '@/components/error-boundary/ErrorBoundary'; import { OfflineToast } from '@/components/toasts'; import { designSystemPlaygroundEnabled, reactNativeDisableYellowBox, showNetworkRequests, showNetworkResponses } from '@/config/debug'; @@ -24,7 +23,6 @@ import store, { AppDispatch, type AppState } from '@/redux/store'; import { MainThemeProvider } from '@/theme/ThemeContext'; import { SharedValuesProvider } from '@/helpers/SharedValuesContext'; import { InitialRouteContext } from '@/navigation/initialRoute'; -import { Portal } from '@/react-native-cool-modals/Portal'; import { NotificationsHandler } from '@/notifications/NotificationsHandler'; import { analyticsV2 } from '@/analytics'; import { getOrCreateDeviceId } from '@/analytics/utils'; @@ -39,7 +37,9 @@ import { RootStackParamList } from '@/navigation/types'; import { IS_ANDROID, IS_DEV } from '@/env'; import { prefetchDefaultFavorites } from '@/resources/favorites'; import Routes from '@/navigation/Routes'; +import { BackupsSync } from '@/state/sync/BackupsSync'; import { BackendNetworks } from '@/components/BackendNetworks'; +import { AbsolutePortalRoot } from './components/AbsolutePortal'; if (IS_DEV) { reactNativeDisableYellowBox && LogBox.ignoreAllLogs(); @@ -67,12 +67,11 @@ function App({ walletReady }: AppProps) { }, []); return ( - + <> {initialRoute && ( - )} @@ -80,14 +79,27 @@ function App({ walletReady }: AppProps) { + - + + ); } -const AppWithRedux = connect(state => ({ - walletReady: state.appState.walletReady, -}))(App); +const AppWithRedux = connect( + state => ({ + walletReady: state.appState.walletReady, + }), + null, + null, + { + areStatesEqual: (next, prev) => { + // Only update if walletReady actually changed + return next.appState.walletReady === prev.appState.walletReady; + }, + areOwnPropsEqual: shallowEqual, + } +)(memo(App)); function Root() { const [initializing, setInitializing] = useState(true); diff --git a/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx b/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx index fef6de976fd..b8e24ae905a 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx @@ -10,15 +10,15 @@ const BlastBadge = require('@/assets/badges/blast.png'); const BscBadge = require('@/assets/badges/bsc.png'); const DegenBadge = require('@/assets/badges/degen.png'); const EthereumBadge = require('@/assets/badges/ethereum.png'); -// const GnosisBadge = require('@/assets/badges/gnosis.png'); -// const GravityBadge = require('@/assets/badges/gravity.png'); +const GnosisBadge = require('@/assets/badges/gnosis.png'); +const GravityBadge = require('@/assets/badges/gravity.png'); const InkBadge = require('@/assets/badges/ink.png'); -// const LineaBadge = require('@/assets/badges/linea.png'); +const LineaBadge = require('@/assets/badges/linea.png'); const OptimismBadge = require('@/assets/badges/optimism.png'); const PolygonBadge = require('@/assets/badges/polygon.png'); -// const SankoBadge = require('@/assets/badges/sanko.png'); -// const ScrollBadge = require('@/assets/badges/scroll.png'); -// const ZksyncBadge = require('@/assets/badges/zksync.png'); +const SankoBadge = require('@/assets/badges/sanko.png'); +const ScrollBadge = require('@/assets/badges/scroll.png'); +const ZksyncBadge = require('@/assets/badges/zksync.png'); const ZoraBadge = require('@/assets/badges/zora.png'); import { ChainId } from '@/state/backendNetworks/types'; @@ -39,20 +39,20 @@ const networkBadges = { [ChainId.bsc]: BscBadge, [ChainId.bscTestnet]: BscBadge, [ChainId.degen]: DegenBadge, - // [ChainId.gnosis]: GnosisBadge, - // [ChainId.gravity]: GravityBadge, + [ChainId.gnosis]: GnosisBadge, + [ChainId.gravity]: GravityBadge, [ChainId.holesky]: EthereumBadge, [ChainId.ink]: InkBadge, - // [ChainId.linea]: LineaBadge, + [ChainId.linea]: LineaBadge, [ChainId.mainnet]: EthereumBadge, [ChainId.optimism]: OptimismBadge, [ChainId.optimismSepolia]: OptimismBadge, [ChainId.polygon]: PolygonBadge, [ChainId.polygonAmoy]: PolygonBadge, - // [ChainId.sanko]: SankoBadge, - // [ChainId.scroll]: ScrollBadge, + [ChainId.sanko]: SankoBadge, + [ChainId.scroll]: ScrollBadge, [ChainId.sepolia]: EthereumBadge, - // [ChainId.zksync]: ZksyncBadge, + [ChainId.zksync]: ZksyncBadge, [ChainId.zora]: ZoraBadge, [ChainId.zoraSepolia]: ZoraBadge, }; diff --git a/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx b/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx index bf382327e4e..15285091a15 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx @@ -17,15 +17,15 @@ import BlastBadge from '@/assets/badges/blast.png'; import BscBadge from '@/assets/badges/bsc.png'; import DegenBadge from '@/assets/badges/degen.png'; import EthereumBadge from '@/assets/badges/ethereum.png'; -// import GnosisBadge from '@/assets/badges/gnosis.png'; -// import GravityBadge from '@/assets/badges/gravity.png'; +import GnosisBadge from '@/assets/badges/gnosis.png'; +import GravityBadge from '@/assets/badges/gravity.png'; import InkBadge from '@/assets/badges/ink.png'; -// import LineaBadge from '@/assets/badges/linea.png'; +import LineaBadge from '@/assets/badges/linea.png'; import OptimismBadge from '@/assets/badges/optimism.png'; import PolygonBadge from '@/assets/badges/polygon.png'; -// import SankoBadge from '@/assets/badges/sanko.png'; -// import ScrollBadge from '@/assets/badges/scroll.png'; -// import ZksyncBadge from '@/assets/badges/zksync.png'; +import SankoBadge from '@/assets/badges/sanko.png'; +import ScrollBadge from '@/assets/badges/scroll.png'; +import ZksyncBadge from '@/assets/badges/zksync.png'; import ZoraBadge from '@/assets/badges/zora.png'; const networkBadges = { @@ -41,20 +41,20 @@ const networkBadges = { [ChainId.bsc]: Image.resolveAssetSource(BscBadge).uri, [ChainId.bscTestnet]: Image.resolveAssetSource(BscBadge).uri, [ChainId.degen]: Image.resolveAssetSource(DegenBadge).uri, - // [ChainId.gnosis]: Image.resolveAssetSource(GnosisBadge).uri, - // [ChainId.gravity]: Image.resolveAssetSource(GravityBadge).uri, + [ChainId.gnosis]: Image.resolveAssetSource(GnosisBadge).uri, + [ChainId.gravity]: Image.resolveAssetSource(GravityBadge).uri, [ChainId.holesky]: Image.resolveAssetSource(EthereumBadge).uri, [ChainId.ink]: Image.resolveAssetSource(InkBadge).uri, - // [ChainId.linea]: Image.resolveAssetSource(LineaBadge).uri, + [ChainId.linea]: Image.resolveAssetSource(LineaBadge).uri, [ChainId.mainnet]: Image.resolveAssetSource(EthereumBadge).uri, [ChainId.optimism]: Image.resolveAssetSource(OptimismBadge).uri, [ChainId.optimismSepolia]: Image.resolveAssetSource(OptimismBadge).uri, [ChainId.polygon]: Image.resolveAssetSource(PolygonBadge).uri, [ChainId.polygonAmoy]: Image.resolveAssetSource(PolygonBadge).uri, - // [ChainId.sanko]: Image.resolveAssetSource(SankoBadge).uri, - // [ChainId.scroll]: Image.resolveAssetSource(ScrollBadge).uri, + [ChainId.sanko]: Image.resolveAssetSource(SankoBadge).uri, + [ChainId.scroll]: Image.resolveAssetSource(ScrollBadge).uri, [ChainId.sepolia]: Image.resolveAssetSource(EthereumBadge).uri, - // [ChainId.zksync]: Image.resolveAssetSource(ZksyncBadge).uri, + [ChainId.zksync]: Image.resolveAssetSource(ZksyncBadge).uri, [ChainId.zora]: Image.resolveAssetSource(ZoraBadge).uri, [ChainId.zoraSepolia]: Image.resolveAssetSource(ZoraBadge).uri, }; diff --git a/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx b/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx index 2c8ba92b2ea..7d8ec66e8e1 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx @@ -14,37 +14,19 @@ import { IS_ANDROID, IS_IOS } from '@/env'; import { PIXEL_RATIO } from '@/utils/deviceUtils'; import { useSwapContext } from '../providers/swap-provider'; -const fallbackIconStyle = { - ...borders.buildCircleAsObject(32), - position: 'absolute' as ViewStyle['position'], -}; - -const largeFallbackIconStyle = { - ...borders.buildCircleAsObject(36), - position: 'absolute' as ViewStyle['position'], -}; - -const smallFallbackIconStyle = { - ...borders.buildCircleAsObject(16), - position: 'absolute' as ViewStyle['position'], -}; - export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({ assetType, - large = true, - small, + size = 32, showBadge = true, }: { assetType: 'input' | 'output'; - large?: boolean; - small?: boolean; + size?: number; showBadge?: boolean; }) { const { isDarkMode, colors } = useTheme(); const { internalSelectedInputAsset, internalSelectedOutputAsset } = useSwapContext(); const asset = assetType === 'input' ? internalSelectedInputAsset : internalSelectedOutputAsset; - const size = small ? 16 : large ? 36 : 32; const didErrorForUniqueId = useSharedValue(undefined); @@ -91,15 +73,8 @@ export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({ })); return ( - - + + {/* ⚠️ TODO: This works but we should figure out how to type this correctly to avoid this error */} {/* @ts-expect-error: Doesn't pick up that it's getting a source prop via animatedProps */} @@ -122,29 +97,14 @@ export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({ /> - - + + @@ -153,28 +113,28 @@ export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({ ); }); +const fallbackIconStyle = (size: number) => ({ + ...borders.buildCircleAsObject(size), + position: 'absolute' as ViewStyle['position'], +}); + +const coinIconFallbackStyle = (size: number) => ({ + borderRadius: size / 2, + height: size, + width: size, + overflow: 'visible' as const, +}); + +const containerStyle = (size: number) => ({ + elevation: 6, + height: size, + overflow: 'visible' as const, +}); + const sx = StyleSheet.create({ coinIcon: { overflow: 'hidden', }, - coinIconFallback: { - borderRadius: 16, - height: 32, - overflow: 'visible', - width: 32, - }, - coinIconFallbackLarge: { - borderRadius: 18, - height: 36, - overflow: 'visible', - width: 36, - }, - coinIconFallbackSmall: { - borderRadius: 8, - height: 16, - overflow: 'visible', - width: 16, - }, container: { elevation: 6, height: 32, diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index c3fb98919b1..dc1894bddf1 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -131,7 +131,7 @@ export function CoinRow({ isFavorite, onPress, output, uniqueId, testID, ...asse iconUrl={icon_url} address={address} mainnetAddress={mainnetAddress} - large + size={36} chainId={chainId} symbol={symbol || ''} color={colors?.primary} diff --git a/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx b/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx index bb5069c5092..f5e15d41c8d 100644 --- a/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx +++ b/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx @@ -24,20 +24,10 @@ const fallbackTextStyles = { textAlign: 'center', }; -const fallbackIconStyle = { - ...borders.buildCircleAsObject(32), +const fallbackIconStyle = (size: number) => ({ + ...borders.buildCircleAsObject(size), position: 'absolute', -}; - -const largeFallbackIconStyle = { - ...borders.buildCircleAsObject(36), - position: 'absolute', -}; - -const smallFallbackIconStyle = { - ...borders.buildCircleAsObject(16), - position: 'absolute', -}; +}); /** * If mainnet asset is available, get the token under /ethereum/ (token) url. @@ -63,22 +53,22 @@ export const SwapCoinIcon = React.memo(function FeedCoinIcon({ iconUrl, disableShadow = true, forceDarkMode, - large, mainnetAddress, chainId, - small, symbol, + size = 32, + chainSize, }: { address: string; color?: string; iconUrl?: string; disableShadow?: boolean; forceDarkMode?: boolean; - large?: boolean; mainnetAddress?: string; chainId: ChainId; - small?: boolean; symbol: string; + size?: number; + chainSize?: number; }) { const theme = useTheme(); @@ -92,52 +82,52 @@ export const SwapCoinIcon = React.memo(function FeedCoinIcon({ const eth = isETH(resolvedAddress); return ( - + {eth ? ( - + ) : ( - + {() => ( )} )} - {chainId && chainId !== ChainId.mainnet && !small && ( + {chainId && chainId !== ChainId.mainnet && size > 16 && ( - + )} ); }); +const styles = { + container: (size: number) => ({ + elevation: 6, + height: size, + overflow: 'visible' as const, + }), + coinIcon: (size: number) => ({ + borderRadius: size / 2, + height: size, + width: size, + overflow: 'visible' as const, + }), +}; + const sx = StyleSheet.create({ badge: { bottom: -0, @@ -151,39 +141,6 @@ const sx = StyleSheet.create({ shadowRadius: 6, shadowOpacity: 0.2, }, - coinIconFallback: { - borderRadius: 16, - height: 32, - overflow: 'visible', - width: 32, - }, - coinIconFallbackLarge: { - borderRadius: 18, - height: 36, - overflow: 'visible', - width: 36, - }, - coinIconFallbackSmall: { - borderRadius: 8, - height: 16, - overflow: 'visible', - width: 16, - }, - container: { - elevation: 6, - height: 32, - overflow: 'visible', - }, - containerLarge: { - elevation: 6, - height: 36, - overflow: 'visible', - }, - containerSmall: { - elevation: 6, - height: 16, - overflow: 'visible', - }, reactCoinIconContainer: { alignItems: 'center', justifyContent: 'center', diff --git a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx index af94a152e8a..23734d39ce8 100644 --- a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx @@ -96,7 +96,7 @@ function SwapInputAmount() { function SwapInputIcon() { return ( - + ); } diff --git a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx index de15b46bee6..93130066142 100644 --- a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx @@ -108,7 +108,7 @@ function SwapOutputAmount({ handleTapWhileDisabled }: { handleTapWhileDisabled: function SwapOutputIcon() { return ( - + ); } diff --git a/src/__swaps__/screens/Swap/components/SwapSlider.tsx b/src/__swaps__/screens/Swap/components/SwapSlider.tsx index 572f9eb6e80..90ac3ac724c 100644 --- a/src/__swaps__/screens/Swap/components/SwapSlider.tsx +++ b/src/__swaps__/screens/Swap/components/SwapSlider.tsx @@ -412,7 +412,7 @@ export const SwapSlider = ({ - + 0 ? query : undefined, @@ -417,7 +417,7 @@ export function useSearchCurrencyLists() { { enabled: memoizedData.enableUnverifiedSearch, select: (data: TokenSearchResult) => { - return getExactMatches(data, query).slice(0, MAX_UNVERIFIED_RESULTS); + return isAddress(query) ? getExactMatches(data, query).slice(0, MAX_UNVERIFIED_RESULTS) : data.slice(0, MAX_UNVERIFIED_RESULTS); }, } ); diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 4d1f66ae1c4..3aca6f27d6c 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -231,6 +231,9 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const isBridge = swapsStore.getState().inputAsset?.mainnetAddress === swapsStore.getState().outputAsset?.mainnetAddress; const isDegenModeEnabled = swapsStore.getState().degenMode; const isSwappingToPopularAsset = swapsStore.getState().outputAsset?.sectionId === 'popular'; + const lastNavigatedTrendingToken = swapsStore.getState().lastNavigatedTrendingToken; + const isSwappingToTrendingAsset = + lastNavigatedTrendingToken === parameters.assetToBuy.uniqueId || lastNavigatedTrendingToken === parameters.assetToSell.uniqueId; const selectedGas = getSelectedGas(parameters.chainId); if (!selectedGas) { @@ -325,6 +328,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { tradeAmountUSD: parameters.quote.tradeAmountUSD, degenMode: isDegenModeEnabled, isSwappingToPopularAsset, + isSwappingToTrendingAsset, errorMessage, isHardwareWallet, }); @@ -389,6 +393,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { tradeAmountUSD: parameters.quote.tradeAmountUSD, degenMode: isDegenModeEnabled, isSwappingToPopularAsset, + isSwappingToTrendingAsset, isHardwareWallet, }); } catch (error) { @@ -403,6 +408,11 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }, }); } + + // reset the last navigated trending token after a swap has taken place + swapsStore.setState({ + lastNavigatedTrendingToken: undefined, + }); }; const executeSwap = performanceTracking.getState().executeFn({ diff --git a/src/__swaps__/screens/Swap/resources/search/discovery.ts b/src/__swaps__/screens/Swap/resources/search/discovery.ts index ebb15d0f59b..40496e0d2d7 100644 --- a/src/__swaps__/screens/Swap/resources/search/discovery.ts +++ b/src/__swaps__/screens/Swap/resources/search/discovery.ts @@ -7,7 +7,7 @@ import { useQuery } from '@tanstack/react-query'; import { parseTokenSearch } from './utils'; const tokenSearchHttp = new RainbowFetchClient({ - baseURL: 'https://token-search.rainbow.me/v3/discovery', + baseURL: 'https://token-search.rainbow.me/v3/trending/swaps', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index b270d55c6cd..52ed1d53443 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -12,7 +12,7 @@ import { parseTokenSearch } from './utils'; const ALL_VERIFIED_TOKENS_PARAM = '/?list=verifiedAssets'; const tokenSearchHttp = new RainbowFetchClient({ - baseURL: 'https://token-search.rainbow.me/v2', + baseURL: 'https://token-search.rainbow.me/v3/tokens', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', @@ -30,13 +30,19 @@ export type TokenSearchArgs = { list: TokenSearchListId; threshold?: TokenSearchThreshold; query?: string; + shouldPersist?: boolean; }; // /////////////////////////////////////////////// // Query Key -const tokenSearchQueryKey = ({ chainId, fromChainId, keys, list, threshold, query }: TokenSearchArgs) => - createQueryKey('TokenSearch', { chainId, fromChainId, keys, list, threshold, query }, { persisterVersion: 2 }); +const tokenSearchQueryKey = ({ chainId, fromChainId, keys, list, threshold, query, shouldPersist }: TokenSearchArgs) => { + return createQueryKey( + 'TokenSearch', + { chainId, fromChainId, keys, list, threshold, query }, + { persisterVersion: shouldPersist ? 3 : undefined } + ); +}; type TokenSearchQueryKey = ReturnType; @@ -77,6 +83,7 @@ async function tokenSearchQueryFunction({ return parseTokenSearch(tokenSearch.data.data, chainId); } + // search for address on other chains const allVerifiedTokens = await tokenSearchHttp.get<{ data: SearchAsset[] }>(ALL_VERIFIED_TOKENS_PARAM); const addressQuery = query.trim().toLowerCase(); @@ -104,8 +111,9 @@ export async function fetchTokenSearch( { chainId, fromChainId, keys, list, threshold, query }: TokenSearchArgs, config: QueryConfigWithSelect = {} ) { + const shouldPersist = query === undefined; return await queryClient.fetchQuery( - tokenSearchQueryKey({ chainId, fromChainId, keys, list, threshold, query }), + tokenSearchQueryKey({ chainId, fromChainId, keys, list, threshold, query, shouldPersist }), tokenSearchQueryFunction, config ); @@ -130,7 +138,8 @@ export function useTokenSearch( { chainId, fromChainId, keys, list, threshold, query }: TokenSearchArgs, config: QueryConfigWithSelect = {} ) { - return useQuery(tokenSearchQueryKey({ chainId, fromChainId, keys, list, threshold, query }), tokenSearchQueryFunction, { + const shouldPersist = query === undefined; + return useQuery(tokenSearchQueryKey({ chainId, fromChainId, keys, list, threshold, query, shouldPersist }), tokenSearchQueryFunction, { ...config, keepPreviousData: true, }); diff --git a/src/analytics/event.ts b/src/analytics/event.ts index adfcbee8a6d..673e71a810d 100644 --- a/src/analytics/event.ts +++ b/src/analytics/event.ts @@ -9,6 +9,7 @@ import { RequestSource } from '@/utils/requestNavigationHandlers'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { AnyPerformanceLog, Screen } from '../state/performance/operations'; import { FavoritedSite } from '@/state/browser/favoriteDappsStore'; +import { TrendingToken } from '@/resources/trendingTokens/trendingTokens'; /** * All events, used by `analytics.track()` @@ -167,6 +168,14 @@ export const event = { // token details tokenDetailsErc20: 'token_details.erc20', tokenDetailsNFT: 'token_details.nft', + + // trending tokens + viewTrendingToken: 'trending_tokens.view_trending_token', + viewRankedCategory: 'trending_tokens.view_ranked_category', + changeNetworkFilter: 'trending_tokens.change_network_filter', + changeTimeframeFilter: 'trending_tokens.change_timeframe_filter', + changeSortFilter: 'trending_tokens.change_sort_filter', + hasLinkedFarcaster: 'trending_tokens.has_linked_farcaster', } as const; type SwapEventParameters = { @@ -186,6 +195,7 @@ type SwapEventParameters = { tradeAmountUSD: number; degenMode: boolean; isSwappingToPopularAsset: boolean; + isSwappingToTrendingAsset: boolean; isHardwareWallet: boolean; }; @@ -706,4 +716,37 @@ export type EventProperties = { eventSentAfterMs: number; available_data: { description: boolean; image_url: boolean; floorPrice: boolean }; }; + + [event.viewTrendingToken]: { + address: TrendingToken['address']; + chainId: TrendingToken['chainId']; + symbol: TrendingToken['symbol']; + name: TrendingToken['name']; + highlightedFriends: number; + }; + + [event.viewRankedCategory]: { + category: string; + chainId: ChainId | undefined; + isLimited: boolean; + isEmpty: boolean; + }; + + [event.changeNetworkFilter]: { + chainId: ChainId | undefined; + }; + + [event.changeTimeframeFilter]: { + timeframe: string; + }; + + [event.changeSortFilter]: { + sort: string | undefined; + }; + + [event.hasLinkedFarcaster]: { + hasFarcaster: boolean; + personalizedTrending: boolean; + walletHash: string; + }; }; diff --git a/src/analytics/userProperties.ts b/src/analytics/userProperties.ts index b42d5518a61..8a467e4b09a 100644 --- a/src/analytics/userProperties.ts +++ b/src/analytics/userProperties.ts @@ -1,3 +1,4 @@ +import { ChainId } from '@/state/backendNetworks/types'; import { NativeCurrencyKey } from '@/entities'; import { Language } from '@/languages'; @@ -36,6 +37,9 @@ export interface UserProperties { hiddenCOins?: string[]; appIcon?: string; + // most used networks at the time the user first opens the network switcher + mostUsedNetworks?: ChainId[]; + // assets NFTs?: number; poaps?: number; diff --git a/src/assets/badges/gnosis.png b/src/assets/badges/gnosis.png new file mode 100644 index 00000000000..afb08cb40e6 Binary files /dev/null and b/src/assets/badges/gnosis.png differ diff --git a/src/assets/badges/gnosis@2x.png b/src/assets/badges/gnosis@2x.png new file mode 100644 index 00000000000..3a021929690 Binary files /dev/null and b/src/assets/badges/gnosis@2x.png differ diff --git a/src/assets/badges/gnosis@3x.png b/src/assets/badges/gnosis@3x.png new file mode 100644 index 00000000000..09946eb88a5 Binary files /dev/null and b/src/assets/badges/gnosis@3x.png differ diff --git a/src/assets/badges/gnosisBadge.png b/src/assets/badges/gnosisBadge.png new file mode 100644 index 00000000000..961610867dd Binary files /dev/null and b/src/assets/badges/gnosisBadge.png differ diff --git a/src/assets/badges/gnosisBadge@2x.png b/src/assets/badges/gnosisBadge@2x.png new file mode 100644 index 00000000000..1355360aca0 Binary files /dev/null and b/src/assets/badges/gnosisBadge@2x.png differ diff --git a/src/assets/badges/gnosisBadge@3x.png b/src/assets/badges/gnosisBadge@3x.png new file mode 100644 index 00000000000..19592fc8d64 Binary files /dev/null and b/src/assets/badges/gnosisBadge@3x.png differ diff --git a/src/assets/badges/gnosisBadgeDark.png b/src/assets/badges/gnosisBadgeDark.png new file mode 100644 index 00000000000..cd7d26152c2 Binary files /dev/null and b/src/assets/badges/gnosisBadgeDark.png differ diff --git a/src/assets/badges/gnosisBadgeDark@2x.png b/src/assets/badges/gnosisBadgeDark@2x.png new file mode 100644 index 00000000000..029c07d9ddb Binary files /dev/null and b/src/assets/badges/gnosisBadgeDark@2x.png differ diff --git a/src/assets/badges/gnosisBadgeDark@3x.png b/src/assets/badges/gnosisBadgeDark@3x.png new file mode 100644 index 00000000000..5c8ed229375 Binary files /dev/null and b/src/assets/badges/gnosisBadgeDark@3x.png differ diff --git a/src/assets/badges/gnosisBadgeLarge.png b/src/assets/badges/gnosisBadgeLarge.png new file mode 100644 index 00000000000..9cf5511d8f1 Binary files /dev/null and b/src/assets/badges/gnosisBadgeLarge.png differ diff --git a/src/assets/badges/gnosisBadgeLarge@2x.png b/src/assets/badges/gnosisBadgeLarge@2x.png new file mode 100644 index 00000000000..301ddfb2914 Binary files /dev/null and b/src/assets/badges/gnosisBadgeLarge@2x.png differ diff --git a/src/assets/badges/gnosisBadgeLarge@3x.png b/src/assets/badges/gnosisBadgeLarge@3x.png new file mode 100644 index 00000000000..e787754b713 Binary files /dev/null and b/src/assets/badges/gnosisBadgeLarge@3x.png differ diff --git a/src/assets/badges/gnosisBadgeLargeDark.png b/src/assets/badges/gnosisBadgeLargeDark.png new file mode 100644 index 00000000000..4f3b0c859cb Binary files /dev/null and b/src/assets/badges/gnosisBadgeLargeDark.png differ diff --git a/src/assets/badges/gnosisBadgeLargeDark@2x.png b/src/assets/badges/gnosisBadgeLargeDark@2x.png new file mode 100644 index 00000000000..79768e5d97e Binary files /dev/null and b/src/assets/badges/gnosisBadgeLargeDark@2x.png differ diff --git a/src/assets/badges/gnosisBadgeLargeDark@3x.png b/src/assets/badges/gnosisBadgeLargeDark@3x.png new file mode 100644 index 00000000000..4975e795ef0 Binary files /dev/null and b/src/assets/badges/gnosisBadgeLargeDark@3x.png differ diff --git a/src/assets/badges/gnosisBadgeNoShadow.png b/src/assets/badges/gnosisBadgeNoShadow.png new file mode 100644 index 00000000000..baa5ee94397 Binary files /dev/null and b/src/assets/badges/gnosisBadgeNoShadow.png differ diff --git a/src/assets/badges/gnosisBadgeNoShadow@2x.png b/src/assets/badges/gnosisBadgeNoShadow@2x.png new file mode 100644 index 00000000000..57e1e098b6a Binary files /dev/null and b/src/assets/badges/gnosisBadgeNoShadow@2x.png differ diff --git a/src/assets/badges/gnosisBadgeNoShadow@3x.png b/src/assets/badges/gnosisBadgeNoShadow@3x.png new file mode 100644 index 00000000000..42a52730e27 Binary files /dev/null and b/src/assets/badges/gnosisBadgeNoShadow@3x.png differ diff --git a/src/assets/badges/gravity.png b/src/assets/badges/gravity.png new file mode 100644 index 00000000000..a515d5551c5 Binary files /dev/null and b/src/assets/badges/gravity.png differ diff --git a/src/assets/badges/gravity@2x.png b/src/assets/badges/gravity@2x.png new file mode 100644 index 00000000000..29e60cbad1c Binary files /dev/null and b/src/assets/badges/gravity@2x.png differ diff --git a/src/assets/badges/gravity@3x.png b/src/assets/badges/gravity@3x.png new file mode 100644 index 00000000000..55108967dea Binary files /dev/null and b/src/assets/badges/gravity@3x.png differ diff --git a/src/assets/badges/gravityBadge.png b/src/assets/badges/gravityBadge.png new file mode 100644 index 00000000000..ad033ad3691 Binary files /dev/null and b/src/assets/badges/gravityBadge.png differ diff --git a/src/assets/badges/gravityBadge@2x.png b/src/assets/badges/gravityBadge@2x.png new file mode 100644 index 00000000000..2264da68ec9 Binary files /dev/null and b/src/assets/badges/gravityBadge@2x.png differ diff --git a/src/assets/badges/gravityBadge@3x.png b/src/assets/badges/gravityBadge@3x.png new file mode 100644 index 00000000000..8d6bdddc381 Binary files /dev/null and b/src/assets/badges/gravityBadge@3x.png differ diff --git a/src/assets/badges/gravityBadgeDark.png b/src/assets/badges/gravityBadgeDark.png new file mode 100644 index 00000000000..02fb9e0677f Binary files /dev/null and b/src/assets/badges/gravityBadgeDark.png differ diff --git a/src/assets/badges/gravityBadgeDark@2x.png b/src/assets/badges/gravityBadgeDark@2x.png new file mode 100644 index 00000000000..9aadd9549ae Binary files /dev/null and b/src/assets/badges/gravityBadgeDark@2x.png differ diff --git a/src/assets/badges/gravityBadgeDark@3x.png b/src/assets/badges/gravityBadgeDark@3x.png new file mode 100644 index 00000000000..5218bf5b88e Binary files /dev/null and b/src/assets/badges/gravityBadgeDark@3x.png differ diff --git a/src/assets/badges/gravityBadgeLarge.png b/src/assets/badges/gravityBadgeLarge.png new file mode 100644 index 00000000000..ccfed23d64b Binary files /dev/null and b/src/assets/badges/gravityBadgeLarge.png differ diff --git a/src/assets/badges/gravityBadgeLarge@2x.png b/src/assets/badges/gravityBadgeLarge@2x.png new file mode 100644 index 00000000000..51e00178021 Binary files /dev/null and b/src/assets/badges/gravityBadgeLarge@2x.png differ diff --git a/src/assets/badges/gravityBadgeLarge@3x.png b/src/assets/badges/gravityBadgeLarge@3x.png new file mode 100644 index 00000000000..805d9fcd7b9 Binary files /dev/null and b/src/assets/badges/gravityBadgeLarge@3x.png differ diff --git a/src/assets/badges/gravityBadgeLargeDark.png b/src/assets/badges/gravityBadgeLargeDark.png new file mode 100644 index 00000000000..ab9ee2fcbae Binary files /dev/null and b/src/assets/badges/gravityBadgeLargeDark.png differ diff --git a/src/assets/badges/gravityBadgeLargeDark@2x.png b/src/assets/badges/gravityBadgeLargeDark@2x.png new file mode 100644 index 00000000000..067b373a7e4 Binary files /dev/null and b/src/assets/badges/gravityBadgeLargeDark@2x.png differ diff --git a/src/assets/badges/gravityBadgeLargeDark@3x.png b/src/assets/badges/gravityBadgeLargeDark@3x.png new file mode 100644 index 00000000000..362d6f3d772 Binary files /dev/null and b/src/assets/badges/gravityBadgeLargeDark@3x.png differ diff --git a/src/assets/badges/gravityBadgeNoShadow.png b/src/assets/badges/gravityBadgeNoShadow.png new file mode 100644 index 00000000000..9492a525822 Binary files /dev/null and b/src/assets/badges/gravityBadgeNoShadow.png differ diff --git a/src/assets/badges/gravityBadgeNoShadow@2x.png b/src/assets/badges/gravityBadgeNoShadow@2x.png new file mode 100644 index 00000000000..5052d829f1e Binary files /dev/null and b/src/assets/badges/gravityBadgeNoShadow@2x.png differ diff --git a/src/assets/badges/gravityBadgeNoShadow@3x.png b/src/assets/badges/gravityBadgeNoShadow@3x.png new file mode 100644 index 00000000000..474f948bf99 Binary files /dev/null and b/src/assets/badges/gravityBadgeNoShadow@3x.png differ diff --git a/src/assets/badges/linea.png b/src/assets/badges/linea.png new file mode 100644 index 00000000000..14e4a7fface Binary files /dev/null and b/src/assets/badges/linea.png differ diff --git a/src/assets/badges/linea@2x.png b/src/assets/badges/linea@2x.png new file mode 100644 index 00000000000..42f41b35f9d Binary files /dev/null and b/src/assets/badges/linea@2x.png differ diff --git a/src/assets/badges/linea@3x.png b/src/assets/badges/linea@3x.png new file mode 100644 index 00000000000..4cc7abd0cdc Binary files /dev/null and b/src/assets/badges/linea@3x.png differ diff --git a/src/assets/badges/lineaBadge.png b/src/assets/badges/lineaBadge.png new file mode 100644 index 00000000000..267723587ca Binary files /dev/null and b/src/assets/badges/lineaBadge.png differ diff --git a/src/assets/badges/lineaBadge@2x.png b/src/assets/badges/lineaBadge@2x.png new file mode 100644 index 00000000000..70bb69fae24 Binary files /dev/null and b/src/assets/badges/lineaBadge@2x.png differ diff --git a/src/assets/badges/lineaBadge@3x.png b/src/assets/badges/lineaBadge@3x.png new file mode 100644 index 00000000000..814892f7fdb Binary files /dev/null and b/src/assets/badges/lineaBadge@3x.png differ diff --git a/src/assets/badges/lineaBadgeDark.png b/src/assets/badges/lineaBadgeDark.png new file mode 100644 index 00000000000..1fa46075d00 Binary files /dev/null and b/src/assets/badges/lineaBadgeDark.png differ diff --git a/src/assets/badges/lineaBadgeDark@2x.png b/src/assets/badges/lineaBadgeDark@2x.png new file mode 100644 index 00000000000..152eb8d6177 Binary files /dev/null and b/src/assets/badges/lineaBadgeDark@2x.png differ diff --git a/src/assets/badges/lineaBadgeDark@3x.png b/src/assets/badges/lineaBadgeDark@3x.png new file mode 100644 index 00000000000..a58ff5b93ac Binary files /dev/null and b/src/assets/badges/lineaBadgeDark@3x.png differ diff --git a/src/assets/badges/lineaBadgeLarge.png b/src/assets/badges/lineaBadgeLarge.png new file mode 100644 index 00000000000..93a23a92347 Binary files /dev/null and b/src/assets/badges/lineaBadgeLarge.png differ diff --git a/src/assets/badges/lineaBadgeLarge@2x.png b/src/assets/badges/lineaBadgeLarge@2x.png new file mode 100644 index 00000000000..0fe5d25acdf Binary files /dev/null and b/src/assets/badges/lineaBadgeLarge@2x.png differ diff --git a/src/assets/badges/lineaBadgeLarge@3x.png b/src/assets/badges/lineaBadgeLarge@3x.png new file mode 100644 index 00000000000..4f3a3c0ce1c Binary files /dev/null and b/src/assets/badges/lineaBadgeLarge@3x.png differ diff --git a/src/assets/badges/lineaBadgeLargeDark.png b/src/assets/badges/lineaBadgeLargeDark.png new file mode 100644 index 00000000000..f463c5abcc7 Binary files /dev/null and b/src/assets/badges/lineaBadgeLargeDark.png differ diff --git a/src/assets/badges/lineaBadgeLargeDark@2x.png b/src/assets/badges/lineaBadgeLargeDark@2x.png new file mode 100644 index 00000000000..f417f8f1f16 Binary files /dev/null and b/src/assets/badges/lineaBadgeLargeDark@2x.png differ diff --git a/src/assets/badges/lineaBadgeLargeDark@3x.png b/src/assets/badges/lineaBadgeLargeDark@3x.png new file mode 100644 index 00000000000..86c51a9e20b Binary files /dev/null and b/src/assets/badges/lineaBadgeLargeDark@3x.png differ diff --git a/src/assets/badges/lineaBadgeNoShadow.png b/src/assets/badges/lineaBadgeNoShadow.png new file mode 100644 index 00000000000..1af3c5fd42d Binary files /dev/null and b/src/assets/badges/lineaBadgeNoShadow.png differ diff --git a/src/assets/badges/lineaBadgeNoShadow@2x.png b/src/assets/badges/lineaBadgeNoShadow@2x.png new file mode 100644 index 00000000000..cddfa99c67f Binary files /dev/null and b/src/assets/badges/lineaBadgeNoShadow@2x.png differ diff --git a/src/assets/badges/lineaBadgeNoShadow@3x.png b/src/assets/badges/lineaBadgeNoShadow@3x.png new file mode 100644 index 00000000000..22007385103 Binary files /dev/null and b/src/assets/badges/lineaBadgeNoShadow@3x.png differ diff --git a/src/assets/badges/sankBadgeDark.png b/src/assets/badges/sankBadgeDark.png new file mode 100644 index 00000000000..6ab341257ab Binary files /dev/null and b/src/assets/badges/sankBadgeDark.png differ diff --git a/src/assets/badges/sankBadgeDark@2x.png b/src/assets/badges/sankBadgeDark@2x.png new file mode 100644 index 00000000000..d4e5beded3f Binary files /dev/null and b/src/assets/badges/sankBadgeDark@2x.png differ diff --git a/src/assets/badges/sankBadgeDark@3x.png b/src/assets/badges/sankBadgeDark@3x.png new file mode 100644 index 00000000000..9b5eba8fe7a Binary files /dev/null and b/src/assets/badges/sankBadgeDark@3x.png differ diff --git a/src/assets/badges/sanko.png b/src/assets/badges/sanko.png new file mode 100644 index 00000000000..e0eff636cac Binary files /dev/null and b/src/assets/badges/sanko.png differ diff --git a/src/assets/badges/sanko@2x.png b/src/assets/badges/sanko@2x.png new file mode 100644 index 00000000000..399f9ad13e6 Binary files /dev/null and b/src/assets/badges/sanko@2x.png differ diff --git a/src/assets/badges/sanko@3x.png b/src/assets/badges/sanko@3x.png new file mode 100644 index 00000000000..c5c5e42e575 Binary files /dev/null and b/src/assets/badges/sanko@3x.png differ diff --git a/src/assets/badges/sankoBadge.png b/src/assets/badges/sankoBadge.png new file mode 100644 index 00000000000..3e44d659f3a Binary files /dev/null and b/src/assets/badges/sankoBadge.png differ diff --git a/src/assets/badges/sankoBadge@2x.png b/src/assets/badges/sankoBadge@2x.png new file mode 100644 index 00000000000..39a74fcdc8d Binary files /dev/null and b/src/assets/badges/sankoBadge@2x.png differ diff --git a/src/assets/badges/sankoBadge@3x.png b/src/assets/badges/sankoBadge@3x.png new file mode 100644 index 00000000000..a9729e07af3 Binary files /dev/null and b/src/assets/badges/sankoBadge@3x.png differ diff --git a/src/assets/badges/sankoBadgeLarge.png b/src/assets/badges/sankoBadgeLarge.png new file mode 100644 index 00000000000..e2ead662090 Binary files /dev/null and b/src/assets/badges/sankoBadgeLarge.png differ diff --git a/src/assets/badges/sankoBadgeLarge@2x.png b/src/assets/badges/sankoBadgeLarge@2x.png new file mode 100644 index 00000000000..726dfc00978 Binary files /dev/null and b/src/assets/badges/sankoBadgeLarge@2x.png differ diff --git a/src/assets/badges/sankoBadgeLarge@3x.png b/src/assets/badges/sankoBadgeLarge@3x.png new file mode 100644 index 00000000000..897a700db31 Binary files /dev/null and b/src/assets/badges/sankoBadgeLarge@3x.png differ diff --git a/src/assets/badges/sankoBadgeLargeDark.png b/src/assets/badges/sankoBadgeLargeDark.png new file mode 100644 index 00000000000..0218596d5bb Binary files /dev/null and b/src/assets/badges/sankoBadgeLargeDark.png differ diff --git a/src/assets/badges/sankoBadgeLargeDark@2x.png b/src/assets/badges/sankoBadgeLargeDark@2x.png new file mode 100644 index 00000000000..40c50ffecf6 Binary files /dev/null and b/src/assets/badges/sankoBadgeLargeDark@2x.png differ diff --git a/src/assets/badges/sankoBadgeLargeDark@3x.png b/src/assets/badges/sankoBadgeLargeDark@3x.png new file mode 100644 index 00000000000..f683ef22d4b Binary files /dev/null and b/src/assets/badges/sankoBadgeLargeDark@3x.png differ diff --git a/src/assets/badges/sankoBadgeNoShadow.png b/src/assets/badges/sankoBadgeNoShadow.png new file mode 100644 index 00000000000..b0b2c0385ac Binary files /dev/null and b/src/assets/badges/sankoBadgeNoShadow.png differ diff --git a/src/assets/badges/sankoBadgeNoShadow@2x.png b/src/assets/badges/sankoBadgeNoShadow@2x.png new file mode 100644 index 00000000000..990e00a88b0 Binary files /dev/null and b/src/assets/badges/sankoBadgeNoShadow@2x.png differ diff --git a/src/assets/badges/sankoBadgeNoShadow@3x.png b/src/assets/badges/sankoBadgeNoShadow@3x.png new file mode 100644 index 00000000000..f79b957c903 Binary files /dev/null and b/src/assets/badges/sankoBadgeNoShadow@3x.png differ diff --git a/src/assets/badges/scroll.png b/src/assets/badges/scroll.png new file mode 100644 index 00000000000..b074e4e54f3 Binary files /dev/null and b/src/assets/badges/scroll.png differ diff --git a/src/assets/badges/scroll@2x.png b/src/assets/badges/scroll@2x.png new file mode 100644 index 00000000000..1dab422416c Binary files /dev/null and b/src/assets/badges/scroll@2x.png differ diff --git a/src/assets/badges/scroll@3x.png b/src/assets/badges/scroll@3x.png new file mode 100644 index 00000000000..04482628c63 Binary files /dev/null and b/src/assets/badges/scroll@3x.png differ diff --git a/src/assets/badges/scrollBadge.png b/src/assets/badges/scrollBadge.png new file mode 100644 index 00000000000..3266ab33b57 Binary files /dev/null and b/src/assets/badges/scrollBadge.png differ diff --git a/src/assets/badges/scrollBadge@2x.png b/src/assets/badges/scrollBadge@2x.png new file mode 100644 index 00000000000..aea2e050f18 Binary files /dev/null and b/src/assets/badges/scrollBadge@2x.png differ diff --git a/src/assets/badges/scrollBadge@3x.png b/src/assets/badges/scrollBadge@3x.png new file mode 100644 index 00000000000..40f77762f36 Binary files /dev/null and b/src/assets/badges/scrollBadge@3x.png differ diff --git a/src/assets/badges/scrollBadgeDark.png b/src/assets/badges/scrollBadgeDark.png new file mode 100644 index 00000000000..71927e891e8 Binary files /dev/null and b/src/assets/badges/scrollBadgeDark.png differ diff --git a/src/assets/badges/scrollBadgeDark@2x.png b/src/assets/badges/scrollBadgeDark@2x.png new file mode 100644 index 00000000000..53095e21774 Binary files /dev/null and b/src/assets/badges/scrollBadgeDark@2x.png differ diff --git a/src/assets/badges/scrollBadgeDark@3x.png b/src/assets/badges/scrollBadgeDark@3x.png new file mode 100644 index 00000000000..5bfd03fdd2b Binary files /dev/null and b/src/assets/badges/scrollBadgeDark@3x.png differ diff --git a/src/assets/badges/scrollBadgeLarge.png b/src/assets/badges/scrollBadgeLarge.png new file mode 100644 index 00000000000..cc1dbc31e63 Binary files /dev/null and b/src/assets/badges/scrollBadgeLarge.png differ diff --git a/src/assets/badges/scrollBadgeLarge@2x.png b/src/assets/badges/scrollBadgeLarge@2x.png new file mode 100644 index 00000000000..c60eca2ff99 Binary files /dev/null and b/src/assets/badges/scrollBadgeLarge@2x.png differ diff --git a/src/assets/badges/scrollBadgeLarge@3x.png b/src/assets/badges/scrollBadgeLarge@3x.png new file mode 100644 index 00000000000..8be695f8247 Binary files /dev/null and b/src/assets/badges/scrollBadgeLarge@3x.png differ diff --git a/src/assets/badges/scrollBadgeLargeDark.png b/src/assets/badges/scrollBadgeLargeDark.png new file mode 100644 index 00000000000..5ecf4f2dff7 Binary files /dev/null and b/src/assets/badges/scrollBadgeLargeDark.png differ diff --git a/src/assets/badges/scrollBadgeLargeDark@2x.png b/src/assets/badges/scrollBadgeLargeDark@2x.png new file mode 100644 index 00000000000..5c6b5d6a50c Binary files /dev/null and b/src/assets/badges/scrollBadgeLargeDark@2x.png differ diff --git a/src/assets/badges/scrollBadgeLargeDark@3x.png b/src/assets/badges/scrollBadgeLargeDark@3x.png new file mode 100644 index 00000000000..0e508374d35 Binary files /dev/null and b/src/assets/badges/scrollBadgeLargeDark@3x.png differ diff --git a/src/assets/badges/scrollBadgeNoShadow.png b/src/assets/badges/scrollBadgeNoShadow.png new file mode 100644 index 00000000000..cfdef2d77c9 Binary files /dev/null and b/src/assets/badges/scrollBadgeNoShadow.png differ diff --git a/src/assets/badges/scrollBadgeNoShadow@2x.png b/src/assets/badges/scrollBadgeNoShadow@2x.png new file mode 100644 index 00000000000..79c1aa6e3b0 Binary files /dev/null and b/src/assets/badges/scrollBadgeNoShadow@2x.png differ diff --git a/src/assets/badges/scrollBadgeNoShadow@3x.png b/src/assets/badges/scrollBadgeNoShadow@3x.png new file mode 100644 index 00000000000..d140debde92 Binary files /dev/null and b/src/assets/badges/scrollBadgeNoShadow@3x.png differ diff --git a/src/assets/badges/zkSyncBadge.png b/src/assets/badges/zkSyncBadge.png new file mode 100644 index 00000000000..f84a89c233f Binary files /dev/null and b/src/assets/badges/zkSyncBadge.png differ diff --git a/src/assets/badges/zkSyncBadge@2x.png b/src/assets/badges/zkSyncBadge@2x.png new file mode 100644 index 00000000000..02a0f753727 Binary files /dev/null and b/src/assets/badges/zkSyncBadge@2x.png differ diff --git a/src/assets/badges/zkSyncBadge@3x.png b/src/assets/badges/zkSyncBadge@3x.png new file mode 100644 index 00000000000..db578e63d79 Binary files /dev/null and b/src/assets/badges/zkSyncBadge@3x.png differ diff --git a/src/assets/badges/zksync.png b/src/assets/badges/zksync.png new file mode 100644 index 00000000000..1225b4e6c6e Binary files /dev/null and b/src/assets/badges/zksync.png differ diff --git a/src/assets/badges/zksync@2x.png b/src/assets/badges/zksync@2x.png new file mode 100644 index 00000000000..b437d430a93 Binary files /dev/null and b/src/assets/badges/zksync@2x.png differ diff --git a/src/assets/badges/zksync@3x.png b/src/assets/badges/zksync@3x.png new file mode 100644 index 00000000000..2e433247e12 Binary files /dev/null and b/src/assets/badges/zksync@3x.png differ diff --git a/src/assets/badges/zksyncBadgeDark.png b/src/assets/badges/zksyncBadgeDark.png new file mode 100644 index 00000000000..b6947dbcd88 Binary files /dev/null and b/src/assets/badges/zksyncBadgeDark.png differ diff --git a/src/assets/badges/zksyncBadgeDark@2x.png b/src/assets/badges/zksyncBadgeDark@2x.png new file mode 100644 index 00000000000..77f075eb22e Binary files /dev/null and b/src/assets/badges/zksyncBadgeDark@2x.png differ diff --git a/src/assets/badges/zksyncBadgeDark@3x.png b/src/assets/badges/zksyncBadgeDark@3x.png new file mode 100644 index 00000000000..a7ea118d5ce Binary files /dev/null and b/src/assets/badges/zksyncBadgeDark@3x.png differ diff --git a/src/assets/badges/zksyncBadgeLarge.png b/src/assets/badges/zksyncBadgeLarge.png new file mode 100644 index 00000000000..02190afd30f Binary files /dev/null and b/src/assets/badges/zksyncBadgeLarge.png differ diff --git a/src/assets/badges/zksyncBadgeLarge@2x.png b/src/assets/badges/zksyncBadgeLarge@2x.png new file mode 100644 index 00000000000..420b0f8531e Binary files /dev/null and b/src/assets/badges/zksyncBadgeLarge@2x.png differ diff --git a/src/assets/badges/zksyncBadgeLarge@3x.png b/src/assets/badges/zksyncBadgeLarge@3x.png new file mode 100644 index 00000000000..25aef546ecf Binary files /dev/null and b/src/assets/badges/zksyncBadgeLarge@3x.png differ diff --git a/src/assets/badges/zksyncBadgeLargeDark.png b/src/assets/badges/zksyncBadgeLargeDark.png new file mode 100644 index 00000000000..19a58296e9a Binary files /dev/null and b/src/assets/badges/zksyncBadgeLargeDark.png differ diff --git a/src/assets/badges/zksyncBadgeLargeDark@2x.png b/src/assets/badges/zksyncBadgeLargeDark@2x.png new file mode 100644 index 00000000000..ea8d6389607 Binary files /dev/null and b/src/assets/badges/zksyncBadgeLargeDark@2x.png differ diff --git a/src/assets/badges/zksyncBadgeLargeDark@3x.png b/src/assets/badges/zksyncBadgeLargeDark@3x.png new file mode 100644 index 00000000000..71a30293a2e Binary files /dev/null and b/src/assets/badges/zksyncBadgeLargeDark@3x.png differ diff --git a/src/assets/badges/zksyncBadgeNoShadow.png b/src/assets/badges/zksyncBadgeNoShadow.png new file mode 100644 index 00000000000..47b5500b5d0 Binary files /dev/null and b/src/assets/badges/zksyncBadgeNoShadow.png differ diff --git a/src/assets/badges/zksyncBadgeNoShadow@2x.png b/src/assets/badges/zksyncBadgeNoShadow@2x.png new file mode 100644 index 00000000000..97f1bc72954 Binary files /dev/null and b/src/assets/badges/zksyncBadgeNoShadow@2x.png differ diff --git a/src/assets/badges/zksyncBadgeNoShadow@3x.png b/src/assets/badges/zksyncBadgeNoShadow@3x.png new file mode 100644 index 00000000000..ed087b5f411 Binary files /dev/null and b/src/assets/badges/zksyncBadgeNoShadow@3x.png differ diff --git a/src/components/AbsolutePortal.tsx b/src/components/AbsolutePortal.tsx index b578234bd0a..a992e84c11c 100644 --- a/src/components/AbsolutePortal.tsx +++ b/src/components/AbsolutePortal.tsx @@ -1,5 +1,5 @@ import React, { PropsWithChildren, ReactNode, useEffect, useState } from 'react'; -import { View } from 'react-native'; +import { StyleProp, ViewStyle, View } from 'react-native'; const absolutePortal = { nodes: [] as ReactNode[], @@ -24,7 +24,7 @@ const absolutePortal = { }, }; -export const AbsolutePortalRoot = () => { +export const AbsolutePortalRoot = ({ style }: { style?: StyleProp }) => { const [nodes, setNodes] = useState(absolutePortal.nodes); useEffect(() => { @@ -32,17 +32,15 @@ export const AbsolutePortalRoot = () => { return () => unsubscribe(); }, []); - return ( - - {nodes} - - ); + return {nodes}; }; export const AbsolutePortal = ({ children }: PropsWithChildren) => { useEffect(() => { absolutePortal.addNode(children); - return () => absolutePortal.removeNode(children); + return () => { + absolutePortal.removeNode(children); + }; }, [children]); return null; diff --git a/src/components/DappBrowser/control-panel/ControlPanel.tsx b/src/components/DappBrowser/control-panel/ControlPanel.tsx index 99c24dd0597..94977e35f14 100644 --- a/src/components/DappBrowser/control-panel/ControlPanel.tsx +++ b/src/components/DappBrowser/control-panel/ControlPanel.tsx @@ -311,7 +311,7 @@ export const ControlPanel = () => { ); }; -const TapToDismiss = memo(function TapToDismiss() { +export const TapToDismiss = memo(function TapToDismiss() { const { goBack } = useNavigation(); return ( diff --git a/src/screens/discover/components/DiscoverFeaturedResultsCard.tsx b/src/components/Discover/DiscoverFeaturedResultsCard.tsx similarity index 100% rename from src/screens/discover/components/DiscoverFeaturedResultsCard.tsx rename to src/components/Discover/DiscoverFeaturedResultsCard.tsx diff --git a/src/screens/discover/components/DiscoverHome.tsx b/src/components/Discover/DiscoverHome.tsx similarity index 88% rename from src/screens/discover/components/DiscoverHome.tsx rename to src/components/Discover/DiscoverHome.tsx index 7132c19c016..8da0ba974af 100644 --- a/src/screens/discover/components/DiscoverHome.tsx +++ b/src/components/Discover/DiscoverHome.tsx @@ -6,9 +6,10 @@ import useExperimentalFlag, { MINTS, NFT_OFFERS, FEATURED_RESULTS, + TRENDING_TOKENS, } from '@rainbow-me/config/experimentalHooks'; import { isTestnetChain } from '@/handlers/web3'; -import { Inline, Inset, Stack, Box } from '@/design-system'; +import { Inline, Inset, Stack, Box, Separator } from '@/design-system'; import { useAccountSettings, useWallets } from '@/hooks'; import { ENSCreateProfileCard } from '@/components/cards/ENSCreateProfileCard'; import { ENSSearchCard } from '@/components/cards/ENSSearchCard'; @@ -28,11 +29,12 @@ import { FeaturedResultStack } from '@/components/FeaturedResult/FeaturedResultS import Routes from '@/navigation/routesNames'; import { useNavigation } from '@/navigation'; import { DiscoverFeaturedResultsCard } from './DiscoverFeaturedResultsCard'; +import { TrendingTokens } from '@/components/Discover/TrendingTokens'; export const HORIZONTAL_PADDING = 20; export default function DiscoverHome() { - const { profiles_enabled, mints_enabled, op_rewards_enabled, featured_results } = useRemoteConfig(); + const { profiles_enabled, mints_enabled, op_rewards_enabled, featured_results, trending_tokens_enabled } = useRemoteConfig(); const { chainId } = useAccountSettings(); const profilesEnabledLocalFlag = useExperimentalFlag(PROFILES); const profilesEnabledRemoteFlag = profiles_enabled; @@ -42,6 +44,7 @@ export default function DiscoverHome() { const mintsEnabled = (useExperimentalFlag(MINTS) || mints_enabled) && !IS_TEST; const opRewardsLocalFlag = useExperimentalFlag(OP_REWARDS); const opRewardsRemoteFlag = op_rewards_enabled; + const trendingTokensEnabled = (useExperimentalFlag(TRENDING_TOKENS) || trending_tokens_enabled) && !IS_TEST; const testNetwork = isTestnetChain({ chainId }); const { navigate } = useNavigation(); const isProfilesEnabled = profilesEnabledLocalFlag && profilesEnabledRemoteFlag; @@ -67,6 +70,13 @@ export default function DiscoverHome() { {isProfilesEnabled && } + + {trendingTokensEnabled && ( + <> + + + + )} {mintsEnabled && ( diff --git a/src/screens/discover/components/DiscoverScreenContent.tsx b/src/components/Discover/DiscoverScreenContent.tsx similarity index 76% rename from src/screens/discover/components/DiscoverScreenContent.tsx rename to src/components/Discover/DiscoverScreenContent.tsx index 1e3a7650013..99271271ded 100644 --- a/src/screens/discover/components/DiscoverScreenContent.tsx +++ b/src/components/Discover/DiscoverScreenContent.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { View } from 'react-native'; import { FlexItem, Page } from '@/components/layout'; -import DiscoverHome from './DiscoverHome'; -import DiscoverSearch from './DiscoverSearch'; -import DiscoverSearchContainer from './DiscoverSearchContainer'; +import DiscoverHome from '@/components/Discover/DiscoverHome'; +import DiscoverSearch from '@/components/Discover/DiscoverSearch'; +import DiscoverSearchContainer from '@/components/Discover/DiscoverSearchContainer'; import { Box, Inset } from '@/design-system'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useDiscoverScreenContext } from '../DiscoverScreenContext'; +import { useDiscoverScreenContext } from '@/components/Discover/DiscoverScreenContext'; function Switcher({ children }: { children: React.ReactNode[] }) { const { isSearching } = useDiscoverScreenContext(); diff --git a/src/screens/discover/DiscoverScreenContext.tsx b/src/components/Discover/DiscoverScreenContext.tsx similarity index 96% rename from src/screens/discover/DiscoverScreenContext.tsx rename to src/components/Discover/DiscoverScreenContext.tsx index eb9b276443d..31a8e89106b 100644 --- a/src/screens/discover/DiscoverScreenContext.tsx +++ b/src/components/Discover/DiscoverScreenContext.tsx @@ -2,6 +2,7 @@ import { analytics } from '@/analytics'; import React, { createContext, Dispatch, SetStateAction, RefObject, useState, useRef, useCallback } from 'react'; import { SectionList, TextInput } from 'react-native'; import Animated from 'react-native-reanimated'; +import { useTrackDiscoverScreenTime } from './useTrackDiscoverScreenTime'; type DiscoverScreenContextType = { scrollViewRef: RefObject; @@ -80,6 +81,8 @@ const DiscoverScreenProvider = ({ children }: { children: React.ReactNode }) => setIsSearching(false); }, [searchQuery]); + useTrackDiscoverScreenTime(); + return ( { + if (!selected) return [fillTertiary, fillTertiary]; + return highlightedBackgroundColor + ? [highlightedBackgroundColor, globalColors.white100] + : [ + isDarkMode ? opacity(globalColors.white100, 0.72) : opacity(fillTertiary, 0.2), + isDarkMode ? opacity(globalColors.white100, 0.92) : opacity(fillTertiary, 0), + ]; + }, [fillTertiary, highlightedBackgroundColor, selected, isDarkMode]); + + return ( + + + {typeof icon === 'string' ? ( + + {icon} + + ) : ( + icon + )} + + {/* This first Text element sets the width of the container */} + + {label} + + {/* This second Text element is the visible label, positioned absolutely within the established frame */} + + {label} + + + + 􀆏 + + + + ); +} + +function useTrendingTokensData() { + const { nativeCurrency } = useAccountSettings(); + const remoteConfig = useRemoteConfig(); + const { chainId, category, timeframe, sort } = useTrendingTokensStore(state => ({ + chainId: state.chainId, + category: state.category, + timeframe: state.timeframe, + sort: state.sort, + })); + + const walletAddress = useFarcasterAccountForWallets(); + + return useTrendingTokens({ + chainId, + category, + timeframe, + sortBy: sort, + sortDirection: SortDirection.Desc, + limit: remoteConfig.trending_tokens_limit, + walletAddress: walletAddress, + currency: nativeCurrency, + }); +} + +function ReportAnalytics() { + const activeSwipeRoute = useNavigationStore(state => state.activeSwipeRoute); + const { category, chainId } = useTrendingTokensStore(state => ({ category: state.category, chainId: state.chainId })); + const { data: trendingTokens, isLoading } = useTrendingTokensData(); + + useEffect(() => { + if (isLoading || activeSwipeRoute !== Routes.DISCOVER_SCREEN) return; + + const isEmpty = (trendingTokens?.length ?? 0) === 0; + const isLimited = !isEmpty && (trendingTokens?.length ?? 0) < 6; + + analyticsV2.track(analyticsV2.event.viewRankedCategory, { + category, + chainId, + isLimited, + isEmpty, + }); + }, [isLoading, activeSwipeRoute, trendingTokens?.length, category, chainId]); + + return null; +} + +function CategoryFilterButton({ + category, + icon, + iconWidth = 16, + iconColor, + label, + highlightedBackgroundColor, +}: { + category: TrendingCategory; + icon: string; + iconColor: string | { default: string; selected: string }; + highlightedBackgroundColor: string; + iconWidth?: number; + label: string; +}) { + const { isDarkMode } = useColorMode(); + const fillTertiary = useBackgroundColor('fillTertiary'); + const separatorSecondary = useForegroundColor('separatorSecondary'); + + const selected = useTrendingTokensStore(state => state.category === category); + + const borderColor = selected && isDarkMode ? globalColors.white80 : separatorSecondary; + + const gradientColors = useMemo(() => { + if (!selected) return [fillTertiary, fillTertiary]; + return [highlightedBackgroundColor, globalColors.white100]; + }, [fillTertiary, highlightedBackgroundColor, selected]); + + const selectCategory = useCallback(() => { + useTrendingTokensStore.getState().setCategory(category); + }, [category]); + + return ( + + + + {icon} + + + {/* This first Text element sets the width of the container */} + + {label} + + {/* This second Text element is the visible label, positioned absolutely within the established frame */} + + {label} + + + + + ); +} + +function FriendPfp({ pfp_url }: { pfp_url: string }) { + const backgroundColor = useBackgroundColor('surfacePrimary'); + return ( + + ); +} +function FriendHolders({ friends }: { friends: FarcasterUser[] }) { + if (friends.length === 0) return null; + const howManyOthers = Math.max(1, friends.length - 2); + const separator = howManyOthers === 1 && friends.length === 2 ? ` ${i18n.t(t.and)} ` : ', '; + + return ( + + + + {friends[1] && } + + + + + {friends[0].username} + {friends[1] && ( + <> + + {separator} + + {friends[1].username} + + )} + + {friends.length > 2 && ( + + {' '} + {i18n.t('trending_tokens.and_others', { count: howManyOthers })} + + )} + + + ); +} + +function TrendingTokenLoadingRow() { + const backgroundColor = useBackgroundColor('surfacePrimary'); + const { isDarkMode } = useColorMode(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function getPriceChangeColor(priceChange: number) { + if (priceChange === 0) return 'labelTertiary'; + return priceChange > 0 ? 'green' : 'red'; +} + +function TrendingTokenRow({ token }: { token: TrendingToken }) { + const separatorColor = useForegroundColor('separator'); + + const price = formatCurrency(token.price); + const marketCap = formatNumber(token.marketCap, { useOrderSuffix: true, decimals: 1, style: '$' }); + const volume = formatNumber(token.volume, { useOrderSuffix: true, decimals: 1, style: '$' }); + + const handleNavigateToToken = useCallback(() => { + analyticsV2.track(analyticsV2.event.viewTrendingToken, { + address: token.address, + chainId: token.chainId, + symbol: token.symbol, + name: token.name, + highlightedFriends: token.highlightedFriends.length, + }); + + swapsStore.setState({ + lastNavigatedTrendingToken: token.uniqueId, + }); + + Navigation.handleAction(Routes.EXPANDED_ASSET_SHEET, { + asset: token, + type: 'token', + }); + }, [token]); + + if (!token) return null; + + return ( + + + + + + + + + + + + {token.name} + + + {token.symbol} + + + {price} + + + + + + + VOL + + + {volume} + + + + + | + + + + + MCAP + + + {marketCap} + + + + + + + + + {formatNumber(token.priceChange.day, { decimals: 2, useOrderSuffix: true })}% + + + + + 1H + + + {formatNumber(token.priceChange.hr, { decimals: 2, useOrderSuffix: true })}% + + + + + + + + ); +} + +function NoResults() { + const { isDarkMode } = useColorMode(); + const fillQuaternary = useBackgroundColor('fillQuaternary'); + const backgroundColor = isDarkMode ? '#191A1C' : fillQuaternary; + + return ( + + + + {i18n.t(t.no_results.title)} + + + {i18n.t(t.no_results.body)} + + + + + 􀙭 + + + + ); +} + +function NetworkFilter() { + const { isDarkMode } = useColorMode(); + const { colors } = useTheme(); + + const selected = useSharedValue(undefined); + const chainId = useTrendingTokensStore(state => state.chainId); + const setChainId = useTrendingTokensStore(state => state.setChainId); + + const { icon, label, lightenedNetworkColor } = useMemo(() => { + if (!chainId) return { icon: '􀤆', label: i18n.t(t.all), lightenedNetworkColor: undefined }; + return { + icon: ( + + + + ), + label: useBackendNetworksStore.getState().getChainsLabel()[chainId], + lightenedNetworkColor: colors.networkColors[chainId] + ? getMixedColor(colors.networkColors[chainId], globalColors.white100, isDarkMode ? 0.55 : 0.6) + : undefined, + }; + }, [chainId, colors.networkColors, isDarkMode]); + + const setSelected = useCallback( + (chainId: ChainId | undefined) => { + 'worklet'; + selected.value = chainId; + runOnJS(setChainId)(chainId); + }, + [selected, setChainId] + ); + + const navigateToNetworkSelector = useCallback(() => { + Navigation.handleAction(Routes.NETWORK_SELECTOR, { + selected, + setSelected, + }); + }, [selected, setSelected]); + + return ( + + ); +} + +function TimeFilter() { + const timeframe = useTrendingTokensStore(state => state.timeframe); + const shouldAbbreviate = timeframe === Timeframe.H24 || timeframe === Timeframe.H12; + + return ( + ({ + actionTitle: i18n.t(t.filters.time[time]), + menuState: time === timeframe ? 'on' : 'off', + actionKey: time, + })), + }} + side="bottom" + onPressMenuItem={timeframe => useTrendingTokensStore.getState().setTimeframe(timeframe)} + > + + + ); +} + +function SortFilter() { + const sort = useTrendingTokensStore(state => state.sort); + const selected = sort !== TrendingSort.Recommended; + + const iconColor = useForegroundColor(selected ? 'labelSecondary' : 'labelTertiary'); + + const sortLabel = useMemo(() => { + if (sort === TrendingSort.Recommended) return i18n.t(t.filters.sort.RECOMMENDED.label); + return i18n.t(t.filters.sort[sort]); + }, [sort]); + + return ( + ({ + actionTitle: s === TrendingSort.Recommended ? i18n.t(t.filters.sort.RECOMMENDED.menuOption) : i18n.t(t.filters.sort[s]), + menuState: s === sort ? 'on' : 'off', + actionKey: s, + })), + }} + side="bottom" + onPressMenuItem={selection => { + if (selection === sort) return useTrendingTokensStore.getState().setSort(TrendingSort.Recommended); + useTrendingTokensStore.getState().setSort(selection); + }} + > + + 􀄬 + + } + /> + + ); +} + +function TrendingTokensLoader() { + const { trending_tokens_limit } = useRemoteConfig(); + + return ( + + {Array.from({ length: trending_tokens_limit }).map((_, index) => ( + + ))} + + ); +} + +function TrendingTokenData() { + const { data: trendingTokens, isLoading } = useTrendingTokensData(); + if (isLoading) return ; + + return ( + } + data={trendingTokens} + renderItem={({ item }) => } + /> + ); +} + +const padding = 20; + +export function TrendingTokens() { + const { isDarkMode } = useColorMode(); + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/Discover/useTrackDiscoverScreenTime.ts b/src/components/Discover/useTrackDiscoverScreenTime.ts new file mode 100644 index 00000000000..b6f9e02f6cf --- /dev/null +++ b/src/components/Discover/useTrackDiscoverScreenTime.ts @@ -0,0 +1,21 @@ +import { useNavigationStore } from '@/state/navigation/navigationStore'; +import { useEffect } from 'react'; +import Routes from '@/navigation/routesNames'; +import { PerformanceTracking, currentlyTrackedMetrics } from '@/performance/tracking'; +import { PerformanceMetrics } from '@/performance/tracking/types/PerformanceMetrics'; + +export const useTrackDiscoverScreenTime = () => { + const isOnDiscoverScreen = useNavigationStore(state => state.isRouteActive(Routes.DISCOVER_SCREEN)); + + useEffect(() => { + const data = currentlyTrackedMetrics.get(PerformanceMetrics.timeSpentOnDiscoverScreen); + + if (!isOnDiscoverScreen && data?.startTimestamp) { + PerformanceTracking.finishMeasuring(PerformanceMetrics.timeSpentOnDiscoverScreen); + } + + if (isOnDiscoverScreen) { + PerformanceTracking.startMeasuring(PerformanceMetrics.timeSpentOnDiscoverScreen); + } + }, [isOnDiscoverScreen]); +}; diff --git a/src/components/ExchangeTokenRow.tsx b/src/components/ExchangeTokenRow.tsx index 0e509debbe4..9fabd7219f5 100644 --- a/src/components/ExchangeTokenRow.tsx +++ b/src/components/ExchangeTokenRow.tsx @@ -2,7 +2,7 @@ import React from 'react'; import isEqual from 'react-fast-compare'; import { Box, Column, Columns, Inline, Stack, Text } from '@/design-system'; import { isNativeAsset } from '@/handlers/assets'; -import { useAsset, useDimensions } from '@/hooks'; +import { useAsset } from '@/hooks'; import { ButtonPressAnimation } from '@/components/animations'; import { FloatingEmojis } from '@/components/floating-emojis'; import { IS_IOS } from '@/env'; @@ -34,7 +34,6 @@ export default React.memo(function ExchangeTokenRow({ disabled, }, }: ExchangeTokenRowProps) { - const { width: deviceWidth } = useDimensions(); const item = useAsset({ address, chainId, @@ -101,10 +100,8 @@ export default React.memo(function ExchangeTokenRow({ {isInfoButtonVisible && } {showFavoriteButton && (IS_IOS ? ( - // @ts-ignore }) { + const blue = useForegroundColor('blue'); + const borderColor = chroma(blue).alpha(0.08).hex(); + + const text = useDerivedValue(() => (editing.value ? translations.done : translations.edit)); + + return ( + { + 'worklet'; + editing.value = !editing.value; + }} + scaleTo={0.95} + style={{ + borderColor, + borderCurve: 'continuous', + borderRadius: 14, + borderWidth: THICK_BORDER_WIDTH, + height: 28, + justifyContent: 'center', + overflow: 'hidden', + paddingHorizontal: 10, + position: 'absolute', + right: 0, + }} + > + + {text} + + + ); +} + +function Header({ editing }: { editing: SharedValue }) { + const separatorTertiary = useForegroundColor('separatorTertiary'); + const fill = useForegroundColor('fill'); + + const title = useDerivedValue(() => { + return editing.value ? translations.edit : translations.networks; + }); + + return ( + + + + + + + + {title} + + + + + + ); +} + +const CustomizeNetworksBanner = !shouldShowCustomizeNetworksBanner(customizeNetworksBannerStore.getState().dismissedAt) + ? () => null + : function CustomizeNetworksBanner({ editing }: { editing: SharedValue }) { + useAnimatedReaction( + () => editing.value, + (editing, prev) => { + if (!prev && editing) runOnJS(dismissCustomizeNetworksBanner)(); + } + ); + + const dismissedAt = customizeNetworksBannerStore(s => s.dismissedAt); + if (!shouldShowCustomizeNetworksBanner(dismissedAt)) return null; + + const height = 75; + const blue = '#268FFF'; + + return ( + + + + + + } + > + + + + + 􀍱 + + + + + {i18n.t(t.customize_networks_banner.title)} + + + {i18n.t(t.customize_networks_banner.tap_the)}{' '} + + {i18n.t(t.edit)} + {' '} + {i18n.t(t.customize_networks_banner.button_to_set_up)} + + + + + 􀆄 + + + + + + + + ); + }; + +const BADGE_BORDER_COLORS = { + default: { + dark: globalColors.white10, + light: '#F2F3F4', + }, + selected: { + dark: '#1E2E40', + light: '#D7E9FD', + }, +}; + +const useNetworkOptionStyle = (isSelected: SharedValue, color?: string) => { + const { isDarkMode } = useColorMode(); + const label = useForegroundColor('labelTertiary'); + + const surfacePrimary = useBackgroundColor('surfacePrimary'); + const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; + const separatorTertiary = useForegroundColor('separatorTertiary'); + + const defaultStyle = { + backgroundColor: isDarkMode ? globalColors.white10 : globalColors.grey20, + borderColor: isDarkMode ? opacity(separatorTertiary, 0.02) : separatorTertiary, + }; + const selectedStyle = { + backgroundColor: chroma + .scale([networkSwitcherBackgroundColor, color || label])(0.16) + .hex(), + borderColor: chroma(color || label) + .alpha(0.16) + .hex(), + }; + + const scale = useSharedValue(1); + useAnimatedReaction( + () => isSelected.value, + (current, prev) => { + if (current === true && prev === false) { + scale.value = withSequence( + withTiming(0.9, { duration: 120, easing: Easing.bezier(0.25, 0.46, 0.45, 0.94) }), + withTiming(1, TIMING_CONFIGS.fadeConfig) + ); + } + } + ); + + const animatedStyle = useAnimatedStyle(() => { + const colors = isSelected.value ? selectedStyle : defaultStyle; + return { + backgroundColor: colors.backgroundColor, + borderColor: colors.borderColor, + transform: [{ scale: scale.value }], + }; + }); + + return { + animatedStyle, + selectedStyle, + defaultStyle, + }; +}; + +function AllNetworksOption({ + selected, + setSelected, +}: { + selected: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; +}) { + const { isDarkMode } = useColorMode(); + const blue = useForegroundColor('blue'); + + const isSelected = useDerivedValue(() => selected.value === undefined); + const { animatedStyle } = useNetworkOptionStyle(isSelected, blue); + + const overlappingBadge = useAnimatedStyle(() => { + return { + borderColor: isSelected.value + ? BADGE_BORDER_COLORS.selected[isDarkMode ? 'dark' : 'light'] + : BADGE_BORDER_COLORS.default[isDarkMode ? 'dark' : 'light'], + }; + }); + + return ( + { + 'worklet'; + setSelected(undefined); + }} + scaleTo={0.95} + > + + + + + + + + + + + + + + + + + {i18n.t(t.all_networks)} + + + + ); +} + +function AllNetworksSection({ + editing, + setSelected, + selected, +}: { + editing: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; + selected: SharedValue; +}) { + const style = useAnimatedStyle(() => ({ + opacity: editing.value ? withTiming(0, TIMING_CONFIGS.fastFadeConfig) : withTiming(1, TIMING_CONFIGS.fastFadeConfig), + height: withTiming( + editing.value ? 0 : ITEM_HEIGHT + 14, // 14 is the gap to the separator + TIMING_CONFIGS.fastFadeConfig + ), + marginTop: editing.value ? 0 : 14, + pointerEvents: editing.value ? 'none' : 'auto', + })); + return ( + + + + + ); +} + +function NetworkOption({ chainId, selected }: { chainId: ChainId; selected: SharedValue }) { + const { colors } = useTheme(); + const chainName = useBackendNetworksStore.getState().getChainsLabel()[chainId]; + const chainColor = colors.networkColors[chainId] ? colors.networkColors[chainId] : undefined; + const isSelected = useDerivedValue(() => selected.value === chainId); + const { animatedStyle } = useNetworkOptionStyle(isSelected, chainColor); + + return ( + + + + {chainName} + + + ); +} + +const SHEET_OUTER_INSET = 8; +const SHEET_INNER_PADDING = 16; +const GAP = 12; +const ITEM_WIDTH = (DEVICE_WIDTH - SHEET_INNER_PADDING * 2 - SHEET_OUTER_INSET * 2 - GAP) / 2; +const ITEM_HEIGHT = 48; +const SEPARATOR_HEIGHT = 68; + +const ALL_NETWORKS_BADGE_SIZE = 16; +const THICKER_BORDER_WIDTH = 5 / 3; + +const enum Section { + pinned, + separator, + unpinned, +} + +function Draggable({ + children, + dragging, + chainId, + networks, + sectionsOffsets, + isUnpinnedHidden, +}: PropsWithChildren<{ + chainId: ChainId; + dragging: SharedValue; + networks: SharedValue>; + sectionsOffsets: SharedValue>; + isUnpinnedHidden: SharedValue; +}>) { + const zIndex = useSharedValue(0); + useAnimatedReaction( + () => dragging.value?.chainId, + (current, prev) => { + if (current === prev) return; + if (current === chainId) zIndex.value = 2; + if (prev === chainId) zIndex.value = 1; + } + ); + + const draggableStyles = useAnimatedStyle(() => { + const section = networks.value[Section.pinned].includes(chainId) ? Section.pinned : Section.unpinned; + const itemIndex = networks.value[section].indexOf(chainId); + const slotPosition = positionFromIndex(itemIndex, sectionsOffsets.value[section]); + + const opacity = + section === Section.unpinned && isUnpinnedHidden.value + ? withTiming(0, TIMING_CONFIGS.fastFadeConfig) + : withDelay(100, withTiming(1, TIMING_CONFIGS.fadeConfig)); + + const isBeingDragged = dragging.value?.chainId === chainId; + const position = isBeingDragged ? dragging.value!.position : slotPosition; + + return { + opacity, + zIndex: zIndex.value, + transform: [ + { scale: withSpring(isBeingDragged ? 1.05 : 1, SPRING_CONFIGS.springConfig) }, + { translateX: isBeingDragged ? position.x : withSpring(position.x, SPRING_CONFIGS.springConfig) }, + { translateY: isBeingDragged ? position.y : withSpring(position.y, SPRING_CONFIGS.springConfig) }, + ], + }; + }); + + return {children}; +} + +const indexFromPosition = (x: number, y: number, offset: { y: number }) => { + 'worklet'; + const yoffsets = y > offset.y ? offset.y : 0; + const column = x > ITEM_WIDTH + GAP / 2 ? 1 : 0; + const row = Math.floor((y - yoffsets) / (ITEM_HEIGHT + GAP)); + const index = row * 2 + column; + return index < 0 ? 0 : index; // row can be negative if the dragged item is above the first row +}; + +const positionFromIndex = (index: number, offset: { y: number }) => { + 'worklet'; + const column = index % 2; + const row = Math.floor(index / 2); + const position = { x: column * (ITEM_WIDTH + GAP), y: row * (ITEM_HEIGHT + GAP) + offset.y }; + return position; +}; + +type Point = { x: number; y: number }; +type DraggingState = { + chainId: ChainId; + position: Point; +}; + +function SectionSeparator({ + sectionsOffsets, + editing, + expanded, + networks, +}: { + sectionsOffsets: SharedValue>; + editing: SharedValue; + expanded: SharedValue; + networks: SharedValue>; +}) { + const pressed = useSharedValue(false); + + const showExpandButtonAsNetworkChip = useDerivedValue(() => { + return !expanded.value && !editing.value && networks.value[Section.pinned].length % 2 !== 0; + }); + + const visible = useDerivedValue(() => { + return networks.value[Section.unpinned].length > 0 || editing.value; + }); + + const tapExpand = Gesture.Tap() + .onTouchesDown((e, s) => { + if (editing.value || !visible.value) return s.fail(); + pressed.value = true; + }) + .onEnd(() => { + pressed.value = false; + expanded.value = !expanded.value; + }); + + const text = useDerivedValue(() => { + if (editing.value) return translations.drag_to_rearrange; + if (showExpandButtonAsNetworkChip.value) return translations.more; + return expanded.value ? translations.show_less : translations.show_more; + }); + + const unpinnedNetworksLength = useDerivedValue(() => networks.value[Section.unpinned].length.toString()); + const showMoreAmountStyle = useAnimatedStyle(() => ({ + opacity: expanded.value || editing.value ? 0 : 1, + })); + const showMoreOrLessIcon = useDerivedValue(() => (expanded.value ? '􀆇' : '􀆈') as string); + const showMoreOrLessIconStyle = useAnimatedStyle(() => ({ opacity: editing.value ? 0 : 1 })); + + const { isDarkMode } = useColorMode(); + + const separatorContainerStyles = useAnimatedStyle(() => { + if (showExpandButtonAsNetworkChip.value) { + const position = positionFromIndex(networks.value[Section.pinned].length, sectionsOffsets.value[Section.pinned]); + return { + backgroundColor: isDarkMode ? globalColors.white10 : globalColors.grey20, + borderColor: '#F5F8FF05', + height: ITEM_HEIGHT, + width: ITEM_WIDTH, + flexDirection: 'row', + alignItems: 'center', + borderRadius: 24, + borderWidth: THICK_BORDER_WIDTH, + transform: [{ translateX: position.x }, { translateY: position.y }], + }; + } + + return { + backgroundColor: 'transparent', + opacity: visible.value ? 1 : 0, + transform: [{ translateY: sectionsOffsets.value[Section.separator].y }, { scale: withTiming(pressed.value ? 0.95 : 1) }], + position: 'absolute', + width: '100%', + height: SEPARATOR_HEIGHT, + }; + }); + + return ( + + + + + {unpinnedNetworksLength} + + + + {text} + + + + {showMoreOrLessIcon} + + + + + ); +} + +function EmptyUnpinnedPlaceholder({ + sectionsOffsets, + networks, + isUnpinnedHidden, +}: { + sectionsOffsets: SharedValue>; + networks: SharedValue>; + isUnpinnedHidden: SharedValue; +}) { + const styles = useAnimatedStyle(() => { + const isVisible = networks.value[Section.unpinned].length === 0 && !isUnpinnedHidden.value; + return { + opacity: isVisible ? withTiming(1, { duration: 800 }) : 0, + transform: [{ translateY: sectionsOffsets.value[Section.unpinned].y }], + }; + }); + const { isDarkMode } = useColorMode(); + return ( + + + {i18n.t(t.drag_here_to_unpin)} + + + ); +} + +function NetworksGrid({ + editing, + setSelected, + selected, +}: { + editing: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; + selected: SharedValue; +}) { + const initialPinned = networkSwitcherStore.getState().pinnedNetworks; + const sortedSupportedChainIds = useBackendNetworksStore.getState().getSortedSupportedChainIds(); + const initialUnpinned = sortedSupportedChainIds.filter(chainId => !initialPinned.includes(chainId)); + const networks = useSharedValue({ [Section.pinned]: initialPinned, [Section.unpinned]: initialUnpinned }); + + useEffect(() => { + // persists pinned networks when closing the sheet + // should be the only time this component is unmounted + return () => { + if (networks.value[Section.pinned].length > 0) { + networkSwitcherStore.setState({ pinnedNetworks: networks.value[Section.pinned] }); + } else { + networkSwitcherStore.setState({ pinnedNetworks: defaultPinnedNetworks }); + } + }; + }, [networks]); + + const expanded = useSharedValue(false); + const isUnpinnedHidden = useDerivedValue(() => !expanded.value && !editing.value); + + const dragging = useSharedValue(null); + + const sectionsOffsets = useDerivedValue(() => { + const pinnedHeight = Math.ceil(networks.value[Section.pinned].length / 2) * (ITEM_HEIGHT + GAP) - GAP; + return { + [Section.pinned]: { y: 0 }, + [Section.separator]: { y: pinnedHeight }, + [Section.unpinned]: { y: pinnedHeight + SEPARATOR_HEIGHT }, + }; + }); + const containerHeight = useDerivedValue(() => { + const length = networks.value[Section.unpinned].length; + const paddingBottom = 32; + const unpinnedHeight = isUnpinnedHidden.value + ? length === 0 + ? -SEPARATOR_HEIGHT + paddingBottom + : 0 + : length === 0 + ? ITEM_HEIGHT + paddingBottom + : Math.ceil((length + 1) / 2) * (ITEM_HEIGHT + GAP) - GAP + paddingBottom; + const height = sectionsOffsets.value[Section.unpinned].y + unpinnedHeight; + return height; + }); + const containerStyle = useAnimatedStyle(() => ({ + height: withDelay(expanded.value ? 0 : 25, withTiming(containerHeight.value, TIMING_CONFIGS.slowerFadeConfig)), + })); + + const dragNetwork = Gesture.Pan() + .maxPointers(1) + .onTouchesDown((e, s) => { + if (!editing.value) { + s.fail(); + return; + } + const touch = e.allTouches[0]; + const section = touch.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; + const sectionOffset = sectionsOffsets.value[section]; + const index = indexFromPosition(touch.x, touch.y, sectionOffset); + const sectionNetworks = networks.value[section]; + const chainId = sectionNetworks[index]; + + if (!chainId || (section === Section.pinned && sectionNetworks.length === 1)) { + s.fail(); + return; + } + + const position = positionFromIndex(index, sectionOffset); + dragging.value = { chainId, position }; + }) + .onChange(e => { + if (!dragging.value) return; + const chainId = dragging.value.chainId; + if (!chainId) return; + + const section = e.y > sectionsOffsets.value[Section.unpinned].y - SEPARATOR_HEIGHT / 2 ? Section.unpinned : Section.pinned; + const sectionArray = networks.value[section]; + + const currentIndex = sectionArray.indexOf(chainId); + const newIndex = Math.min(indexFromPosition(e.x, e.y, sectionsOffsets.value[section]), sectionArray.length - 1); + + networks.modify(networks => { + if (currentIndex === -1) { + // Pin/Unpin + if (section === Section.unpinned) networks[Section.pinned].splice(currentIndex, 1); + else networks[Section.pinned].push(chainId); + networks[Section.unpinned] = sortedSupportedChainIds.filter(chainId => !networks[Section.pinned].includes(chainId)); + } else if (section === Section.pinned && newIndex !== currentIndex) { + // Reorder + networks[Section.pinned].splice(currentIndex, 1); + networks[Section.pinned].splice(newIndex, 0, chainId); + } + return networks; + }); + dragging.modify(dragging => { + if (!dragging) return dragging; + dragging.position.x += e.changeX; + dragging.position.y += e.changeY; + return dragging; + }); + }) + .onFinalize(() => { + dragging.value = null; + }); + + const tapNetwork = Gesture.Tap() + .onTouchesDown((e, s) => { + if (editing.value) return s.fail(); + }) + .onEnd(e => { + const section = e.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; + const index = indexFromPosition(e.x, e.y, sectionsOffsets.value[section]); + const chainId = networks.value[section][index]; + if (!chainId) return; + + setSelected(chainId); + }); + + const gridGesture = Gesture.Exclusive(dragNetwork, tapNetwork); + + return ( + + + {initialPinned.map(chainId => ( + + + + ))} + + + + + + {initialUnpinned.map(chainId => ( + + + + ))} + + + ); +} + +function Sheet({ children, editing, onClose }: PropsWithChildren<{ editing: SharedValue; onClose: VoidFunction }>) { + const { isDarkMode } = useColorMode(); + const surfacePrimary = useBackgroundColor('surfacePrimary'); + const backgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; + const separatorSecondary = useForegroundColor('separatorSecondary'); + + // make sure the onClose function is called when the sheet unmounts + useEffect(() => { + return () => onClose?.(); + }, [onClose]); + + return ( + <> + +
+ {children} + + + + ); +} + +export function NetworkSelector() { + const { + params: { onClose = noop, selected, setSelected }, + } = useRoute>(); + + const editing = useSharedValue(false); + + return ( + + + + + + ); +} + +const sx = StyleSheet.create({ + overlappingBadge: { + borderWidth: THICKER_BORDER_WIDTH, + borderRadius: ALL_NETWORKS_BADGE_SIZE, + marginLeft: -9, + width: ALL_NETWORKS_BADGE_SIZE + THICKER_BORDER_WIDTH * 2, + height: ALL_NETWORKS_BADGE_SIZE + THICKER_BORDER_WIDTH * 2, + }, + sheet: { + flex: 1, + width: deviceUtils.dimensions.width - 16, + bottom: Math.max(safeAreaInsetValues.bottom + 5, IS_IOS ? 8 : 30), + pointerEvents: 'box-none', + position: 'absolute', + zIndex: 30000, + left: 8, + right: 8, + paddingHorizontal: 16, + borderCurve: 'continuous', + borderRadius: 42, + borderWidth: THICK_BORDER_WIDTH, + overflow: 'hidden', + }, +}); diff --git a/src/components/PortalConsumer.js b/src/components/PortalConsumer.js deleted file mode 100644 index 351b2271f02..00000000000 --- a/src/components/PortalConsumer.js +++ /dev/null @@ -1,18 +0,0 @@ -import React, { useEffect } from 'react'; -import { LoadingOverlay } from './modal'; -import { useWallets } from '@/hooks'; -import { sheetVerticalOffset } from '@/navigation/effects'; -import { usePortal } from '@/react-native-cool-modals/Portal'; - -export default function PortalConsumer() { - const { isWalletLoading } = useWallets(); - const { setComponent, hide } = usePortal(); - useEffect(() => { - if (isWalletLoading) { - setComponent(, true); - } - return hide; - }, [hide, isWalletLoading, setComponent]); - - return null; -} diff --git a/src/components/WalletLoadingListener.tsx b/src/components/WalletLoadingListener.tsx new file mode 100644 index 00000000000..6a9e605ab4f --- /dev/null +++ b/src/components/WalletLoadingListener.tsx @@ -0,0 +1,17 @@ +import React, { useEffect } from 'react'; +import { LoadingOverlay } from './modal'; +import { sheetVerticalOffset } from '@/navigation/effects'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; + +export default function WalletLoadingListener() { + const loadingState = walletLoadingStore(state => state.loadingState); + + useEffect(() => { + if (loadingState) { + walletLoadingStore.getState().setComponent(); + } + return walletLoadingStore.getState().hide; + }, [loadingState]); + + return null; +} diff --git a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx index 4ea959efb7c..3239c50dbda 100644 --- a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx +++ b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx @@ -16,24 +16,23 @@ import BscBadge from '@/assets/badges/bscBadge.png'; import BscBadgeDark from '@/assets/badges/bscBadgeDark.png'; import DegenBadge from '@/assets/badges/degenBadge.png'; import DegenBadgeDark from '@/assets/badges/degenBadgeDark.png'; -// import GnosisBadge from '@/assets/badges/gnosisBadge.png'; -// import GnosisBadgeDark from '@/assets/badges/gnosisBadgeDark.png'; -// import GravityBadge from '@/assets/badges/gravityBadge.png'; -// import GravityBadgeDark from '@/assets/badges/gravityBadgeDark.png'; +import GnosisBadge from '@/assets/badges/gnosisBadge.png'; +import GnosisBadgeDark from '@/assets/badges/gnosisBadgeDark.png'; +import GravityBadge from '@/assets/badges/gravityBadge.png'; +import GravityBadgeDark from '@/assets/badges/gravityBadgeDark.png'; import InkBadge from '@/assets/badges/inkBadge.png'; import InkBadgeDark from '@/assets/badges/inkBadgeDark.png'; -// import LineaBadge from '@/assets/badges/lineaBadge.png'; -// import LineaBadgeDark from '@/assets/badges/lineaBadgeDark.png'; +import LineaBadge from '@/assets/badges/lineaBadge.png'; +import LineaBadgeDark from '@/assets/badges/lineaBadgeDark.png'; import OptimismBadge from '@/assets/badges/optimismBadge.png'; import OptimismBadgeDark from '@/assets/badges/optimismBadgeDark.png'; import PolygonBadge from '@/assets/badges/polygonBadge.png'; import PolygonBadgeDark from '@/assets/badges/polygonBadgeDark.png'; -// import SankoBadge from '@/assets/badges/sankoBadge.png'; -// import SankoBadgeDark from '@/assets/badges/sankoBadgeDark.png'; -// import ScrollBadge from '@/assets/badges/scrollBadge.png'; -// import ScrollBadgeDark from '@/assets/badges/scrollBadgeDark.png'; -// import ZksyncBadge from '@/assets/badges/zksyncBadge.png'; -// import ZksyncBadgeDark from '@/assets/badges/zksyncBadgeDark.png'; +import SankoBadge from '@/assets/badges/sankoBadge.png'; +import ScrollBadge from '@/assets/badges/scrollBadge.png'; +import ScrollBadgeDark from '@/assets/badges/scrollBadgeDark.png'; +import ZksyncBadge from '@/assets/badges/zkSyncBadge.png'; +import ZksyncBadgeDark from '@/assets/badges/zksyncBadgeDark.png'; import ZoraBadge from '@/assets/badges/zoraBadge.png'; import ZoraBadgeDark from '@/assets/badges/zoraBadgeDark.png'; @@ -76,22 +75,22 @@ const AssetIconsByTheme: { dark: DegenBadgeDark, light: DegenBadge, }, - // [ChainId.gnosis]: { - // dark: GnosisBadgeDark, - // light: GnosisBadge, - // }, - // [ChainId.gravity]: { - // dark: GravityBadgeDark, - // light: GravityBadge, - // }, + [ChainId.gnosis]: { + dark: GnosisBadgeDark, + light: GnosisBadge, + }, + [ChainId.gravity]: { + dark: GravityBadgeDark, + light: GravityBadge, + }, [ChainId.ink]: { dark: InkBadgeDark, light: InkBadge, }, - // [ChainId.linea]: { - // dark: LineaBadgeDark, - // light: LineaBadge, - // }, + [ChainId.linea]: { + dark: LineaBadgeDark, + light: LineaBadge, + }, [ChainId.optimism]: { dark: OptimismBadgeDark, light: OptimismBadge, @@ -100,18 +99,18 @@ const AssetIconsByTheme: { dark: PolygonBadgeDark, light: PolygonBadge, }, - // [ChainId.sanko]: { - // dark: SankoBadgeDark, - // light: SankoBadge, - // }, - // [ChainId.scroll]: { - // dark: ScrollBadgeDark, - // light: ScrollBadge, - // }, - // [ChainId.zksync]: { - // dark: ZksyncBadgeDark, - // light: ZksyncBadge, - // }, + [ChainId.sanko]: { + dark: SankoBadge, + light: SankoBadge, + }, + [ChainId.scroll]: { + dark: ScrollBadgeDark, + light: ScrollBadge, + }, + [ChainId.zksync]: { + dark: ZksyncBadgeDark, + light: ZksyncBadge, + }, [ChainId.zora]: { dark: ZoraBadgeDark, light: ZoraBadge, diff --git a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx index d122b9a622e..49bf81fd43e 100644 --- a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx @@ -174,10 +174,8 @@ export default React.memo(function FastCurrencySelectionRow({ {showFavoriteButton && chainId === ChainId.mainnet && (ios ? ( - // @ts-ignore - + @@ -216,13 +216,17 @@ function SendButton() { ); } -export function MoreButton() { - // //////////////////////////////////////////////////// - // Handlers - +export function CopyButton() { const [isToastActive, setToastActive] = useRecoilState(addressCopiedToastAtom); const { accountAddress } = useAccountProfile(); + const { isDamaged } = useWallets(); + const handlePressCopy = React.useCallback(() => { + if (isDamaged) { + showWalletErrorAlert(); + return; + } + if (!isToastActive) { setToastActive(true); setTimeout(() => { @@ -230,7 +234,7 @@ export function MoreButton() { }, 2000); } Clipboard.setString(accountAddress); - }, [accountAddress, isToastActive, setToastActive]); + }, [accountAddress, isDamaged, isToastActive, setToastActive]); return ( <> diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx index 616935fe6ff..9859a4ae105 100644 --- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx @@ -109,7 +109,6 @@ export function ProfileNameRow({ scaleTo={0} size={50} wiggleFactor={0} - // @ts-expect-error – JS component setOnNewEmoji={newOnNewEmoji => (onNewEmoji.current = newOnNewEmoji)} /> diff --git a/src/components/backup/AddWalletToCloudBackupStep.tsx b/src/components/backup/AddWalletToCloudBackupStep.tsx deleted file mode 100644 index 62f92a99e2f..00000000000 --- a/src/components/backup/AddWalletToCloudBackupStep.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { useCallback } from 'react'; -import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system'; -import * as lang from '@/languages'; -import { ImgixImage } from '../images'; -import WalletsAndBackupIcon from '@/assets/WalletsAndBackup.png'; -import { Source } from 'react-native-fast-image'; -import { cloudPlatform } from '@/utils/platform'; -import { ButtonPressAnimation } from '../animations'; -import Routes from '@/navigation/routesNames'; -import { useNavigation } from '@/navigation'; -import { useWallets } from '@/hooks'; -import { WalletCountPerType, useVisibleWallets } from '@/screens/SettingsSheet/useVisibleWallets'; -import { format } from 'date-fns'; -import { useCreateBackup } from './useCreateBackup'; -import { login } from '@/handlers/cloudBackup'; - -const imageSize = 72; - -export default function AddWalletToCloudBackupStep() { - const { goBack } = useNavigation(); - const { wallets, selectedWallet } = useWallets(); - - const walletTypeCount: WalletCountPerType = { - phrase: 0, - privateKey: 0, - }; - - const { lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount }); - - const { onSubmit } = useCreateBackup({ - walletId: selectedWallet.id, - navigateToRoute: { - route: Routes.SETTINGS_SHEET, - params: { - screen: Routes.SETTINGS_SECTION_BACKUP, - }, - }, - }); - - const potentiallyLoginAndSubmit = useCallback(async () => { - await login(); - return onSubmit({}); - }, [onSubmit]); - - const onMaybeLater = useCallback(() => goBack(), [goBack]); - - return ( - - - - - - {lang.t(lang.l.back_up.cloud.add_wallet_to_cloud_backups)} - - - - - - - - - potentiallyLoginAndSubmit().then(success => success && goBack())}> - - - - - 􀎽{' '} - {lang.t(lang.l.back_up.cloud.back_to_cloud_platform_now, { - cloudPlatform, - })} - - - - - - - - - - - - - - - - {lang.t(lang.l.back_up.cloud.mayber_later)} - - - - - - - - - - - {lastBackupDate && ( - - - - - {lang.t(lang.l.back_up.cloud.latest_backup, { - date: format(lastBackupDate, "M/d/yy 'at' h:mm a"), - })} - - - - - )} - - ); -} diff --git a/src/components/backup/BackupManuallyStep.tsx b/src/components/backup/BackupManuallyStep.tsx deleted file mode 100644 index da18d73806a..00000000000 --- a/src/components/backup/BackupManuallyStep.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { useCallback } from 'react'; -import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system'; -import * as lang from '@/languages'; -import { ImgixImage } from '../images'; -import ManuallyBackedUpIcon from '@/assets/ManuallyBackedUp.png'; -import { Source } from 'react-native-fast-image'; -import { ButtonPressAnimation } from '../animations'; -import { useNavigation } from '@/navigation'; -import Routes from '@/navigation/routesNames'; -import { useWallets } from '@/hooks'; -import walletTypes from '@/helpers/walletTypes'; -import { SETTINGS_BACKUP_ROUTES } from '@/screens/SettingsSheet/components/Backups/routes'; -import walletBackupTypes from '@/helpers/walletBackupTypes'; - -const imageSize = 72; - -export default function BackupManuallyStep() { - const { navigate, goBack } = useNavigation(); - const { selectedWallet } = useWallets(); - - const onManualBackup = async () => { - const title = - selectedWallet?.imported && selectedWallet.type === walletTypes.privateKey - ? (selectedWallet.addresses || [])[0].label - : selectedWallet.name; - - goBack(); - navigate(Routes.SETTINGS_SHEET, { - screen: SETTINGS_BACKUP_ROUTES.SECRET_WARNING, - params: { - isBackingUp: true, - title, - backupType: walletBackupTypes.manual, - walletId: selectedWallet.id, - }, - }); - }; - - const onMaybeLater = useCallback(() => goBack(), [goBack]); - - return ( - - - - - - {lang.t(lang.l.back_up.manual.backup_manually_now)} - - - - - - - - - - - - - - {lang.t(lang.l.back_up.manual.back_up_now)} - - - - - - - - - - - - - - - - {lang.t(lang.l.back_up.manual.already_backed_up)} - - - - - - - - - - - ); -} diff --git a/src/components/backup/BackupSheet.tsx b/src/components/backup/BackupSheet.tsx index 5b6a0a4300a..12e15c60190 100644 --- a/src/components/backup/BackupSheet.tsx +++ b/src/components/backup/BackupSheet.tsx @@ -2,13 +2,10 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import React, { useCallback } from 'react'; import { BackupCloudStep, RestoreCloudStep } from '.'; import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes'; -import BackupChooseProviderStep from '@/components/backup/BackupChooseProviderStep'; +import BackupWalletPrompt from '@/components/backup/BackupWalletPrompt'; import { BackgroundProvider } from '@/design-system'; import { SimpleSheet } from '@/components/sheet/SimpleSheet'; -import AddWalletToCloudBackupStep from '@/components/backup/AddWalletToCloudBackupStep'; -import BackupManuallyStep from './BackupManuallyStep'; import { getHeightForStep } from '@/navigation/config'; -import { CloudBackupProvider } from './CloudBackupProvider'; type BackupSheetParams = { BackupSheet: { @@ -21,38 +18,32 @@ type BackupSheetParams = { }; export default function BackupSheet() { - const { params: { step = WalletBackupStepTypes.no_provider } = {} } = useRoute>(); + const { params: { step = WalletBackupStepTypes.backup_prompt } = {} } = useRoute>(); const renderStep = useCallback(() => { switch (step) { - case WalletBackupStepTypes.backup_now_to_cloud: - return ; - case WalletBackupStepTypes.backup_now_manually: - return ; case WalletBackupStepTypes.backup_cloud: return ; case WalletBackupStepTypes.restore_from_backup: return ; - case WalletBackupStepTypes.no_provider: + case WalletBackupStepTypes.backup_prompt: default: - return ; + return ; } }, [step]); return ( - - - {({ backgroundColor }) => ( - - {renderStep()} - - )} - - + + {({ backgroundColor }) => ( + + {renderStep()} + + )} + ); } diff --git a/src/components/backup/BackupChooseProviderStep.tsx b/src/components/backup/BackupWalletPrompt.tsx similarity index 66% rename from src/components/backup/BackupChooseProviderStep.tsx rename to src/components/backup/BackupWalletPrompt.tsx index 38325639704..77071534f48 100644 --- a/src/components/backup/BackupChooseProviderStep.tsx +++ b/src/components/backup/BackupWalletPrompt.tsx @@ -1,7 +1,6 @@ -import React from 'react'; -import { useCreateBackup } from '@/components/backup/useCreateBackup'; +import React, { useCallback, useMemo } from 'react'; import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system'; -import * as lang from '@/languages'; +import * as i18n from '@/languages'; import { ImgixImage } from '../images'; import WalletsAndBackupIcon from '@/assets/WalletsAndBackup.png'; import ManuallyBackedUpIcon from '@/assets/ManuallyBackedUp.png'; @@ -14,13 +13,13 @@ import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { SETTINGS_BACKUP_ROUTES } from '@/screens/SettingsSheet/components/Backups/routes'; import { useWallets } from '@/hooks'; -import walletTypes from '@/helpers/walletTypes'; +import WalletTypes from '@/helpers/walletTypes'; import walletBackupTypes from '@/helpers/walletBackupTypes'; -import { IS_ANDROID } from '@/env'; -import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup'; -import { WrappedAlert as Alert } from '@/helpers/alert'; -import { RainbowError, logger } from '@/logger'; -import { Linking } from 'react-native'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; +import { TextColor } from '@/design-system/color/palettes'; +import { CustomColor } from '@/design-system/color/useForegroundColor'; const imageSize = 72; @@ -28,67 +27,31 @@ export default function BackupSheetSectionNoProvider() { const { colors } = useTheme(); const { navigate, goBack } = useNavigation(); const { selectedWallet } = useWallets(); + const createBackup = useCreateBackup(); + const { status } = backupsStore(state => ({ + status: state.status, + })); - const { onSubmit, loading } = useCreateBackup({ - walletId: selectedWallet.id, - navigateToRoute: { - route: Routes.SETTINGS_SHEET, - params: { - screen: Routes.SETTINGS_SECTION_BACKUP, - }, - }, - }); - - const onCloudBackup = async () => { - if (loading !== 'none') { - return; - } - // NOTE: On Android we need to make sure the user is signed into a Google account before trying to backup - // otherwise we'll fake backup and it's confusing... - if (IS_ANDROID) { - try { - await login(); - getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { - if (!accountDetails) { - Alert.alert(lang.t(lang.l.back_up.errors.no_account_found)); - return; - } - }); - } catch (e) { - logger.error(new RainbowError('[BackupSheetSectionNoProvider]: No account found'), { - error: e, - }); - Alert.alert(lang.t(lang.l.back_up.errors.no_account_found)); - } - } else { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - Alert.alert( - lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.label), - lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.description), - [ - { - onPress: () => { - Linking.openURL('https://support.apple.com/en-us/HT204025'); - }, - text: lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.show_me), - }, - { - style: 'cancel', - text: lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), - }, - ] - ); - return; - } - } + const onCloudBackup = useCallback(() => { + // pop the bottom sheet, and navigate to the backup section inside settings sheet + goBack(); + navigate(Routes.SETTINGS_SHEET, { + screen: Routes.SETTINGS_SECTION_BACKUP, + initial: false, + }); - onSubmit({}); - }; + executeFnIfCloudBackupAvailable({ + fn: () => + createBackup({ + walletId: selectedWallet.id, + }), + logout: true, + }); + }, [createBackup, goBack, navigate, selectedWallet.id]); - const onManualBackup = async () => { + const onManualBackup = useCallback(async () => { const title = - selectedWallet?.imported && selectedWallet.type === walletTypes.privateKey + selectedWallet?.imported && selectedWallet.type === WalletTypes.privateKey ? (selectedWallet.addresses || [])[0].label : selectedWallet.name; @@ -102,13 +65,38 @@ export default function BackupSheetSectionNoProvider() { walletId: selectedWallet.id, }, }); - }; + }, [goBack, navigate, selectedWallet.addresses, selectedWallet.id, selectedWallet?.imported, selectedWallet.name, selectedWallet.type]); + + const isCloudBackupDisabled = useMemo(() => { + return status !== CloudBackupState.Ready && status !== CloudBackupState.NotAvailable; + }, [status]); + + const { color, text } = useMemo<{ text: string; color: TextColor | CustomColor }>(() => { + if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) { + return { + text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled), + color: 'primary (Deprecated)', + }; + } + + if (status === CloudBackupState.Ready) { + return { + text: i18n.t(i18n.l.back_up.cloud.cloud_backup), + color: 'primary (Deprecated)', + }; + } + + return { + text: i18n.t(i18n.l.back_up.cloud.statuses.syncing), + color: 'yellow', + }; + }, [status]); return ( - {lang.t(lang.l.back_up.cloud.how_would_you_like_to_backup)} + {i18n.t(i18n.l.back_up.cloud.how_would_you_like_to_backup)} @@ -116,8 +104,7 @@ export default function BackupSheetSectionNoProvider() { - {/* replace this with BackUpMenuButton */} - + @@ -133,18 +120,18 @@ export default function BackupSheetSectionNoProvider() { marginRight={{ custom: -12 }} marginTop={{ custom: 0 }} marginBottom={{ custom: -8 }} - source={WalletsAndBackupIcon as Source} + source={WalletsAndBackupIcon} width={{ custom: imageSize }} size={imageSize} /> - - {lang.t(lang.l.back_up.cloud.cloud_backup)} + + {text} - {lang.t(lang.l.back_up.cloud.recommended_for_beginners)} + {i18n.t(i18n.l.back_up.cloud.recommended_for_beginners)} {' '} - {lang.t(lang.l.back_up.cloud.choose_backup_cloud_description, { + {i18n.t(i18n.l.back_up.cloud.choose_backup_cloud_description, { cloudPlatform, })} @@ -192,10 +179,10 @@ export default function BackupSheetSectionNoProvider() { size={imageSize} /> - {lang.t(lang.l.back_up.cloud.manual_backup)} + {i18n.t(i18n.l.back_up.cloud.manual_backup)} - {lang.t(lang.l.back_up.cloud.choose_backup_manual_description)} + {i18n.t(i18n.l.back_up.cloud.choose_backup_manual_description)} diff --git a/src/components/backup/ChooseBackupStep.tsx b/src/components/backup/ChooseBackupStep.tsx index 2f7b68cedf6..d08d4cdb0e2 100644 --- a/src/components/backup/ChooseBackupStep.tsx +++ b/src/components/backup/ChooseBackupStep.tsx @@ -6,26 +6,24 @@ import { useDimensions } from '@/hooks'; import { useNavigation } from '@/navigation'; import styled from '@/styled-thing'; import { margin, padding } from '@/styles'; -import { Box, Stack, Text } from '@/design-system'; -import { RouteProp, useRoute } from '@react-navigation/native'; +import { Box, Stack } from '@/design-system'; import { sharedCoolModalTopOffset } from '@/navigation/config'; -import { ImgixImage } from '../images'; +import { ImgixImage } from '@/components/images'; import MenuContainer from '@/screens/SettingsSheet/components/MenuContainer'; import Menu from '@/screens/SettingsSheet/components/Menu'; import { format } from 'date-fns'; import MenuItem from '@/screens/SettingsSheet/components/MenuItem'; import Routes from '@/navigation/routesNames'; -import { Backup, parseTimestampFromFilename } from '@/model/backup'; -import { RestoreSheetParams } from '@/screens/RestoreSheet'; +import { BackupFile, parseTimestampFromFilename } from '@/model/backup'; import { Source } from 'react-native-fast-image'; import { IS_ANDROID } from '@/env'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import useCloudBackups, { CloudBackupStep } from '@/hooks/useCloudBackups'; -import { Centered } from '../layout'; -import { cloudPlatform } from '@/utils/platform'; -import Spinner from '../Spinner'; -import ActivityIndicator from '../ActivityIndicator'; +import { Page } from '@/components/layout'; +import Spinner from '@/components/Spinner'; +import ActivityIndicator from '@/components/ActivityIndicator'; import { useTheme } from '@/theme'; +import { backupsStore, CloudBackupState, LoadingStates } from '@/state/backups/backups'; +import { titleForBackupState } from '@/screens/SettingsSheet/utils'; const Title = styled(RNText).attrs({ align: 'left', @@ -53,60 +51,27 @@ const Masthead = styled(Box).attrs({ }); export function ChooseBackupStep() { - const { - params: { fromSettings }, - } = useRoute>(); const { colors } = useTheme(); - const { isFetching, backups, userData, step, fetchBackups } = useCloudBackups(); + const { status, backups, mostRecentBackup } = backupsStore(state => ({ + status: state.status, + backups: state.backups, + mostRecentBackup: state.mostRecentBackup, + })); + + const isLoading = LoadingStates.includes(status); const { top } = useSafeAreaInsets(); const { height: deviceHeight } = useDimensions(); const { navigate } = useNavigation(); - const cloudBackups = backups.files - .filter(backup => { - if (IS_ANDROID) { - return !backup.name.match(/UserData/i); - } - - return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i); - }) - .sort((a, b) => { - return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); - }); - - const mostRecentBackup = cloudBackups.reduce( - (prev, current) => { - if (!current) { - return prev; - } - - if (!prev) { - return current; - } - - const prevTimestamp = new Date(prev.lastModified).getTime(); - const currentTimestamp = new Date(current.lastModified).getTime(); - if (currentTimestamp > prevTimestamp) { - return current; - } - - return prev; - }, - undefined as Backup | undefined - ); - const onSelectCloudBackup = useCallback( - (selectedBackup: Backup) => { + (selectedBackup: BackupFile) => { navigate(Routes.RESTORE_CLOUD_SHEET, { - backups, - userData, selectedBackup, - fromSettings, }); }, - [navigate, userData, backups, fromSettings] + [navigate] ); const height = IS_ANDROID ? deviceHeight - top : deviceHeight - sharedCoolModalTopOffset - 48; @@ -132,7 +97,7 @@ export function ChooseBackupStep() { - {!isFetching && step === CloudBackupStep.FAILED && ( + {status === CloudBackupState.FailedToInitialize && ( backupsStore.getState().syncAndFetchBackups()} titleComponent={} /> )} - {!isFetching && !cloudBackups.length && step !== CloudBackupStep.FAILED && ( + {status === CloudBackupState.Ready && backups.files.length === 0 && ( + + + } + /> + + + - } /> + backupsStore.getState().syncAndFetchBackups()} + titleComponent={} + /> )} - {!isFetching && cloudBackups.length > 0 && ( + {status === CloudBackupState.Ready && backups.files.length > 0 && ( {mostRecentBackup && ( - - } - onPress={() => onSelectCloudBackup(mostRecentBackup)} - size={52} - width="full" - titleComponent={} - /> - + + + } + onPress={() => onSelectCloudBackup(mostRecentBackup)} + size={52} + width="full" + titleComponent={} + /> + + )} - - {cloudBackups.map( - backup => - backup.name !== mostRecentBackup?.name && ( + + + + {backups.files + .filter(backup => backup.name !== mostRecentBackup?.name) + .sort((a, b) => { + const timestampA = new Date(parseTimestampFromFilename(a.name)).getTime(); + const timestampB = new Date(parseTimestampFromFilename(b.name)).getTime(); + return timestampB - timestampA; + }) + .map(backup => ( + onSelectCloudBackup(backup)} + size={52} + width="full" + titleComponent={ + + } + /> + ))} + {backups.files.length === 1 && ( onSelectCloudBackup(backup)} + disabled size={52} - width="full" - titleComponent={ - - } + titleComponent={} /> - ) - )} + )} + + - {cloudBackups.length === 1 && ( + } + width="full" + onPress={() => backupsStore.getState().syncAndFetchBackups()} + titleComponent={} /> - )} - + + )} - {isFetching && ( - + {isLoading && ( + {android ? : } - - {lang.t(lang.l.back_up.cloud.fetching_backups, { - cloudPlatformName: cloudPlatform, - })} - - + {titleForBackupState[status]} + )} diff --git a/src/components/backup/CloudBackupProvider.tsx b/src/components/backup/CloudBackupProvider.tsx deleted file mode 100644 index 377e9d13a83..00000000000 --- a/src/components/backup/CloudBackupProvider.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'; -import type { BackupUserData, CloudBackups } from '@/model/backup'; -import { - fetchAllBackups, - fetchUserDataFromCloud, - getGoogleAccountUserData, - isCloudBackupAvailable, - syncCloud, -} from '@/handlers/cloudBackup'; -import { RainbowError, logger } from '@/logger'; -import { IS_ANDROID } from '@/env'; - -type CloudBackupContext = { - isFetching: boolean; - backups: CloudBackups; - fetchBackups: () => Promise; - userData: BackupUserData | undefined; -}; - -const CloudBackupContext = createContext({} as CloudBackupContext); - -export function CloudBackupProvider({ children }: PropsWithChildren) { - const [isFetching, setIsFetching] = useState(false); - const [backups, setBackups] = useState({ - files: [], - }); - - const [userData, setUserData] = useState(); - - const fetchBackups = async () => { - try { - setIsFetching(true); - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - logger.debug('[CloudBackupProvider]: Cloud backup is not available'); - setIsFetching(false); - return; - } - - if (IS_ANDROID) { - const gdata = await getGoogleAccountUserData(); - if (!gdata) { - return; - } - } - - logger.debug('[CloudBackupProvider]: Syncing with cloud'); - await syncCloud(); - - logger.debug('[CloudBackupProvider]: Fetching user data'); - const userData = await fetchUserDataFromCloud(); - setUserData(userData); - - logger.debug('[CloudBackupProvider]: Fetching all backups'); - const backups = await fetchAllBackups(); - - logger.debug(`[CloudBackupProvider]: Retrieved ${backups.files.length} backup files`); - setBackups(backups); - } catch (e) { - logger.error(new RainbowError('[CloudBackupProvider]: Failed to fetch all backups'), { - error: e, - }); - } - setIsFetching(false); - }; - - useEffect(() => { - fetchBackups(); - }, []); - - const value = { - isFetching, - backups, - fetchBackups, - userData, - }; - - return {children}; -} - -export function useCloudBackups() { - const context = useContext(CloudBackupContext); - if (context === null) { - throw new Error('useCloudBackups must be used within a CloudBackupProvider'); - } - return context; -} diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index e8bd83aa7a3..ce0774f2ec3 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -6,8 +6,7 @@ import WalletAndBackup from '@/assets/WalletsAndBackup.png'; import { KeyboardArea } from 'react-native-keyboard-area'; import { - Backup, - fetchBackupPassword, + BackupFile, getLocalBackupPassword, restoreCloudBackup, RestoreCloudBackupResultStates, @@ -17,10 +16,10 @@ import { cloudPlatform } from '@/utils/platform'; import { PasswordField } from '../fields'; import { Text } from '../text'; import { WrappedAlert as Alert } from '@/helpers/alert'; -import { cloudBackupPasswordMinLength, isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; +import { isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { useDimensions, useInitializeWallet } from '@/hooks'; -import { useNavigation } from '@/navigation'; +import { Navigation, useNavigation } from '@/navigation'; import { addressSetSelected, setAllWalletsWithIdsAsBackedUp, walletsLoadState, walletsSetSelected } from '@/redux/wallets'; import Routes from '@/navigation/routesNames'; import styled from '@/styled-thing'; @@ -35,8 +34,16 @@ import RainbowButtonTypes from '../buttons/rainbow-button/RainbowButtonTypes'; import { RouteProp, useRoute } from '@react-navigation/native'; import { RestoreSheetParams } from '@/screens/RestoreSheet'; import { Source } from 'react-native-fast-image'; -import { useTheme } from '@/theme'; -import useCloudBackups from '@/hooks/useCloudBackups'; +import { ThemeContextProps, useTheme } from '@/theme'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; +import { isEmpty } from 'lodash'; +import { backupsStore } from '@/state/backups/backups'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; + +type ComponentProps = { + theme: ThemeContextProps; + color: ThemeContextProps['colors'][keyof ThemeContextProps['colors']]; +}; const Title = styled(Text).attrs({ size: 'big', @@ -45,7 +52,7 @@ const Title = styled(Text).attrs({ ...padding.object(12, 0, 0), }); -const DescriptionText = styled(Text).attrs(({ theme: { colors }, color }: any) => ({ +const DescriptionText = styled(Text).attrs(({ theme: { colors }, color }: ComponentProps) => ({ align: 'left', color: color || colors.alpha(colors.blueGreyDark, 0.5), lineHeight: 'looser', @@ -53,7 +60,7 @@ const DescriptionText = styled(Text).attrs(({ theme: { colors }, color }: any) = weight: 'medium', }))({}); -const ButtonText = styled(Text).attrs(({ theme: { colors }, color }: any) => ({ +const ButtonText = styled(Text).attrs(({ theme: { colors }, color }: ComponentProps) => ({ align: 'center', letterSpacing: 'rounded', color: color || colors.alpha(colors.blueGreyDark, 0.5), @@ -71,38 +78,46 @@ const Masthead = styled(Box).attrs({ }); const KeyboardSizeView = styled(KeyboardArea)({ - backgroundColor: ({ theme: { colors } }: any) => colors.transparent, + backgroundColor: ({ theme: { colors } }: ComponentProps) => colors.transparent, }); type RestoreCloudStepParams = { RestoreSheet: { - selectedBackup: Backup; + selectedBackup: BackupFile; }; }; export default function RestoreCloudStep() { const { params } = useRoute>(); + const { password } = backupsStore(state => ({ + password: state.password, + })); - const { userData } = useCloudBackups(); + const loadingState = walletLoadingStore(state => state.loadingState); const { selectedBackup } = params; const { isDarkMode } = useTheme(); - const [loading, setLoading] = useState(false); + const { canGoBack, goBack } = useNavigation(); + + const onRestoreSuccess = useCallback(() => { + while (canGoBack()) { + goBack(); + } + }, [canGoBack, goBack]); const dispatch = useDispatch(); const { width: deviceWidth, height: deviceHeight } = useDimensions(); - const { replace, navigate, getState: dangerouslyGetState, goBack } = useNavigation(); const [validPassword, setValidPassword] = useState(false); const [incorrectPassword, setIncorrectPassword] = useState(false); - const [password, setPassword] = useState(''); const passwordRef = useRef(null); const initializeWallet = useInitializeWallet(); useEffect(() => { const fetchPasswordIfPossible = async () => { - const pwd = await fetchBackupPassword(); + const pwd = await getLocalBackupPassword(); if (pwd) { - setPassword(pwd); + backupsStore.getState().setStoredPassword(pwd); + backupsStore.getState().setPassword(pwd); } }; fetchPasswordIfPossible(); @@ -118,35 +133,42 @@ export default function RestoreCloudStep() { }, [incorrectPassword, password]); const onPasswordChange = useCallback(({ nativeEvent: { text: inputText } }: { nativeEvent: { text: string } }) => { - setPassword(inputText); + backupsStore.getState().setPassword(inputText); setIncorrectPassword(false); }, []); const onSubmit = useCallback(async () => { - setLoading(true); + // NOTE: Localizing password to prevent an empty string from being saved if we re-render + const pwd = password.trim(); + let filename = selectedBackup.name; + + const prevWalletsState = await dispatch(walletsLoadState()); + try { if (!selectedBackup.name) { throw new Error('No backup file selected'); } - const prevWalletsState = await dispatch(walletsLoadState()); - + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.RESTORING_WALLET, + }); const status = await restoreCloudBackup({ - password, - userData, - nameOfSelectedBackupFile: selectedBackup.name, + password: pwd, + backupFilename: filename, }); - if (status === RestoreCloudBackupResultStates.success) { // Store it in the keychain in case it was missing - const hasSavedPassword = await getLocalBackupPassword(); - if (!hasSavedPassword) { - await saveLocalBackupPassword(password); + if (backupsStore.getState().storedPassword !== pwd) { + await saveLocalBackupPassword(pwd); + } + + // Reset the storedPassword state for next restoration process + if (backupsStore.getState().storedPassword) { + backupsStore.getState().setStoredPassword(''); } InteractionManager.runAfterInteractions(async () => { const newWalletsState = await dispatch(walletsLoadState()); - let filename = selectedBackup.name; if (IS_ANDROID && filename) { filename = normalizeAndroidBackupFilename(filename); } @@ -188,14 +210,21 @@ export default function RestoreCloudStep() { const p2 = dispatch(addressSetSelected(firstAddress)); await Promise.all([p1, p2]); await initializeWallet(null, null, null, false, false, null, true, null); - - const operation = dangerouslyGetState()?.index === 1 ? navigate : replace; - operation(Routes.SWIPE_LAYOUT, { - screen: Routes.WALLET_SCREEN, - }); - - setLoading(false); }); + + onRestoreSuccess(); + backupsStore.getState().setPassword(''); + if (isEmpty(prevWalletsState)) { + Navigation.handleAction( + Routes.SWIPE_LAYOUT, + { + screen: Routes.WALLET_SCREEN, + }, + true + ); + } else { + Navigation.handleAction(Routes.WALLET_SCREEN, {}); + } } else { switch (status) { case RestoreCloudBackupResultStates.incorrectPassword: @@ -211,18 +240,17 @@ export default function RestoreCloudStep() { } } catch (e) { Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); + } finally { + walletLoadingStore.setState({ + loadingState: null, + }); } - - setLoading(false); - }, [selectedBackup.name, password, userData, dispatch, initializeWallet, dangerouslyGetState, navigate, replace]); + }, [password, selectedBackup.name, dispatch, onRestoreSuccess, initializeWallet]); const onPasswordSubmit = useCallback(() => { validPassword && onSubmit(); }, [onSubmit, validPassword]); - const isPasswordValid = - (password !== '' && password.length < cloudBackupPasswordMinLength && !passwordRef?.current?.isFocused()) || incorrectPassword; - return ( @@ -248,8 +276,8 @@ export default function RestoreCloudStep() { ; }; }; -export type useCreateBackupStateType = 'none' | 'loading' | 'success' | 'error'; +type ConfirmBackupProps = { + password: string; +} & UseCreateBackupProps; -export enum BackupTypes { - Single = 'single', - All = 'all', -} - -export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupProps) => { +export const useCreateBackup = () => { const dispatch = useDispatch(); const { navigate } = useNavigation(); - const { fetchBackups } = useCloudBackups(); const walletCloudBackup = useWalletCloudBackup(); const { wallets } = useWallets(); - const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]); - const [loading, setLoading] = useState('none'); - - const [password, setPassword] = useState(''); const setLoadingStateWithTimeout = useCallback( - (state: useCreateBackupStateType, resetInMS = 2500) => { - setLoading(state); + ({ state, outOfSync = false, failInMs = 10_000 }: { state: CloudBackupState; outOfSync?: boolean; failInMs?: number }) => { + backupsStore.getState().setStatus(state); + if (outOfSync) { + setTimeout(() => { + backupsStore.getState().setStatus(CloudBackupState.Syncing); + }, 1_000); + } setTimeout(() => { - setLoading('none'); - }, resetInMS); + const currentState = backupsStore.getState().status; + if (currentState === state) { + backupsStore.getState().setStatus(CloudBackupState.Ready); + } + }, failInMs); }, - [setLoading] + [] + ); + + const onSuccess = useCallback( + async (password: string) => { + if (backupsStore.getState().storedPassword !== password) { + await saveLocalBackupPassword(password); + } + // Reset the storedPassword state for next backup + backupsStore.getState().setStoredPassword(''); + analytics.track('Backup Complete', { + category: 'backup', + label: cloudPlatform, + }); + setLoadingStateWithTimeout({ + state: CloudBackupState.Success, + outOfSync: true, + }); + backupsStore.getState().syncAndFetchBackups(); + }, + [setLoadingStateWithTimeout] ); - const onSuccess = useCallback(async () => { - const hasSavedPassword = await getLocalBackupPassword(); - if (!hasSavedPassword) { - await saveLocalBackupPassword(password); - } - analytics.track('Backup Complete', { - category: 'backup', - label: cloudPlatform, - }); - setLoadingStateWithTimeout('success'); - fetchBackups(); - }, [setLoadingStateWithTimeout, fetchBackups, password]); const onError = useCallback( - (msg: string) => { + (msg: string, isDamaged?: boolean) => { InteractionManager.runAfterInteractions(async () => { - DelayedAlert({ title: msg }, 500); - setLoadingStateWithTimeout('error', 5000); + if (isDamaged) { + showWalletErrorAlert(); + } else { + DelayedAlert({ title: msg }, 500); + } + setLoadingStateWithTimeout({ state: CloudBackupState.Error }); }); }, [setLoadingStateWithTimeout] ); const onConfirmBackup = useCallback( - async ({ password, type }: { password: string; type: BackupTypes }) => { + async ({ password, walletId, navigateToRoute }: ConfirmBackupProps) => { analytics.track('Tapped "Confirm Backup"'); - setLoading('loading'); + backupsStore.getState().setStatus(CloudBackupState.InProgress); - if (type === BackupTypes.All) { + if (typeof walletId === 'undefined') { if (!wallets) { - onError('Error loading wallets. Please try again.'); - setLoading('error'); + onError(i18n.t(i18n.l.back_up.errors.no_keys_found)); + backupsStore.getState().setStatus(CloudBackupState.Error); + return; + } + + const validWallets = Object.fromEntries(Object.entries(wallets).filter(([_, wallet]) => !wallet.damaged)); + if (Object.keys(validWallets).length === 0) { + onError(i18n.t(i18n.l.back_up.errors.no_keys_found), true); + backupsStore.getState().setStatus(CloudBackupState.Error); return; } + backupAllWalletsToCloud({ - wallets: wallets as AllRainbowWallets, + wallets: validWallets, password, - latestBackup, onError, onSuccess, dispatch, @@ -94,12 +114,6 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr return; } - if (!walletId) { - onError('Wallet not found. Please try again.'); - setLoading('error'); - return; - } - await walletCloudBackup({ onError, onSuccess, @@ -111,13 +125,13 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr navigate(navigateToRoute.route, navigateToRoute.params || {}); } }, - [walletId, walletCloudBackup, onError, onSuccess, navigateToRoute, wallets, latestBackup, dispatch, navigate] + [walletCloudBackup, onError, wallets, onSuccess, dispatch, navigate] ); - const getPassword = useCallback(async (): Promise => { + const getPassword = useCallback(async (props: UseCreateBackupProps): Promise => { const password = await getLocalBackupPassword(); if (password) { - setPassword(password); + backupsStore.getState().setStoredPassword(password); return password; } @@ -126,32 +140,36 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr nativeScreen: true, step: walletBackupStepTypes.backup_cloud, onSuccess: async (password: string) => { - setPassword(password); - resolve(password); + return resolve(password); }, onCancel: async () => { - resolve(null); + return resolve(null); }, - walletId, + ...props, }); }); - }, [walletId]); + }, []); - const onSubmit = useCallback( - async ({ type = BackupTypes.Single }: { type?: BackupTypes }) => { - const password = await getPassword(); - if (password) { - onConfirmBackup({ - password, - type, + const createBackup = useCallback( + async (props: UseCreateBackupProps) => { + if (backupsStore.getState().status !== CloudBackupState.Ready) { + return false; + } + const password = await getPassword(props); + if (!password) { + setLoadingStateWithTimeout({ + state: CloudBackupState.Ready, }); - return true; + return false; } - setLoadingStateWithTimeout('error'); - return false; + onConfirmBackup({ + password, + ...props, + }); + return true; }, [getPassword, onConfirmBackup, setLoadingStateWithTimeout] ); - return { onSuccess, onError, onSubmit, loading }; + return createBackup; }; diff --git a/src/components/cards/OpRewardsCard.tsx b/src/components/cards/OpRewardsCard.tsx index 3c1251010b3..b823be5e198 100644 --- a/src/components/cards/OpRewardsCard.tsx +++ b/src/components/cards/OpRewardsCard.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { GenericCard, Gradient } from './GenericCard'; -import { AccentColorProvider, Box, Cover, globalColors, Stack, Text } from '@/design-system'; +import { AccentColorProvider, Box, Cover, globalColors, Stack, Text, useColorMode } from '@/design-system'; import { ButtonPressAnimation } from '@/components/animations'; -import { Image } from 'react-native'; +import { ImageBackground } from 'react-native'; import OpRewardsCardBackgroundImage from '../../assets/opRewardsCardBackgroundImage.png'; import * as i18n from '@/languages'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; -import { colors } from '@/styles'; import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const GRADIENT: Gradient = { colors: ['#520907', '#B22824'], @@ -18,17 +18,20 @@ const GRADIENT: Gradient = { export const OpRewardsCard: React.FC = () => { const { navigate } = useNavigation(); + const { isDarkMode } = useColorMode(); + + const color = useBackendNetworksStore.getState().getColorsForChainId(ChainId.optimism, isDarkMode); const navigateToRewardsSheet = () => { navigate(Routes.OP_REWARDS_SHEET); }; return ( - + { - + {i18n.t(i18n.l.discover.op_rewards.button_title)} diff --git a/src/components/coin-icon/ChainBadge.js b/src/components/coin-icon/ChainBadge.js index 8babe5015a6..2dbd8061cf2 100644 --- a/src/components/coin-icon/ChainBadge.js +++ b/src/components/coin-icon/ChainBadge.js @@ -29,22 +29,22 @@ import DegenBadge from '@/assets/badges/degenBadge.png'; import DegenBadgeDark from '@/assets/badges/degenBadgeDark.png'; import DegenBadgeLarge from '@/assets/badges/degenBadgeLarge.png'; import DegenBadgeLargeDark from '@/assets/badges/degenBadgeLargeDark.png'; -// import GnosisBadge from '@/assets/badges/gnosisBadge.png'; -// import GnosisBadgeDark from '@/assets/badges/gnosisBadgeDark.png'; -// import GnosisBadgeLarge from '@/assets/badges/gnosisBadgeLarge.png'; -// import GnosisBadgeLargeDark from '@/assets/badges/gnosisBadgeLargeDark.png'; -// import GravityBadge from '@/assets/badges/gravityBadge.png'; -// import GravityBadgeDark from '@/assets/badges/gravityBadgeDark.png'; -// import GravityBadgeLarge from '@/assets/badges/gravityBadgeLarge.png'; -// import GravityBadgeLargeDark from '@/assets/badges/gravityBadgeLargeDark.png'; +import GnosisBadge from '@/assets/badges/gnosisBadge.png'; +import GnosisBadgeDark from '@/assets/badges/gnosisBadgeDark.png'; +import GnosisBadgeLarge from '@/assets/badges/gnosisBadgeLarge.png'; +import GnosisBadgeLargeDark from '@/assets/badges/gnosisBadgeLargeDark.png'; +import GravityBadge from '@/assets/badges/gravityBadge.png'; +import GravityBadgeDark from '@/assets/badges/gravityBadgeDark.png'; +import GravityBadgeLarge from '@/assets/badges/gravityBadgeLarge.png'; +import GravityBadgeLargeDark from '@/assets/badges/gravityBadgeLargeDark.png'; import InkBadge from '@/assets/badges/inkBadge.png'; import InkBadgeDark from '@/assets/badges/inkBadgeDark.png'; import InkBadgeLarge from '@/assets/badges/inkBadgeLarge.png'; import InkBadgeLargeDark from '@/assets/badges/inkBadgeLargeDark.png'; -// import LineaBadge from '@/assets/badges/lineaBadge.png'; -// import LineaBadgeDark from '@/assets/badges/lineaBadgeDark.png'; -// import LineaBadgeLarge from '@/assets/badges/lineaBadgeLarge.png'; -// import LineaBadgeLargeDark from '@/assets/badges/lineaBadgeLargeDark.png'; +import LineaBadge from '@/assets/badges/lineaBadge.png'; +import LineaBadgeDark from '@/assets/badges/lineaBadgeDark.png'; +import LineaBadgeLarge from '@/assets/badges/lineaBadgeLarge.png'; +import LineaBadgeLargeDark from '@/assets/badges/lineaBadgeLargeDark.png'; import OptimismBadge from '@/assets/badges/optimismBadge.png'; import OptimismBadgeDark from '@/assets/badges/optimismBadgeDark.png'; import OptimismBadgeLarge from '@/assets/badges/optimismBadgeLarge.png'; @@ -53,18 +53,17 @@ import PolygonBadge from '@/assets/badges/polygonBadge.png'; import PolygonBadgeDark from '@/assets/badges/polygonBadgeDark.png'; import PolygonBadgeLarge from '@/assets/badges/polygonBadgeLarge.png'; import PolygonBadgeLargeDark from '@/assets/badges/polygonBadgeLargeDark.png'; -// import SankoBadge from '@/assets/badges/sankoBadge.png'; -// import SankoBadgeDark from '@/assets/badges/sankoBadgeDark.png'; -// import SankoBadgeLarge from '@/assets/badges/sankoBadgeLarge.png'; -// import SankoBadgeLargeDark from '@/assets/badges/sankoBadgeLargeDark.png'; -// import ScrollBadge from '@/assets/badges/scrollBadge.png'; -// import ScrollBadgeDark from '@/assets/badges/scrollBadgeDark.png'; -// import ScrollBadgeLarge from '@/assets/badges/scrollBadgeLarge.png'; -// import ScrollBadgeLargeDark from '@/assets/badges/scrollBadgeLargeDark.png'; -// import ZksyncBadge from '@/assets/badges/zksyncBadge.png'; -// import ZksyncBadgeDark from '@/assets/badges/zksyncBadgeDark.png'; -// import ZksyncBadgeLarge from '@/assets/badges/zksyncBadgeLarge.png'; -// import ZksyncBadgeLargeDark from '@/assets/badges/zksyncBadgeLargeDark.png'; +import SankoBadge from '@/assets/badges/sankoBadge.png'; +import SankoBadgeLarge from '@/assets/badges/sankoBadgeLarge.png'; +import SankoBadgeLargeDark from '@/assets/badges/sankoBadgeLargeDark.png'; +import ScrollBadge from '@/assets/badges/scrollBadge.png'; +import ScrollBadgeDark from '@/assets/badges/scrollBadgeDark.png'; +import ScrollBadgeLarge from '@/assets/badges/scrollBadgeLarge.png'; +import ScrollBadgeLargeDark from '@/assets/badges/scrollBadgeLargeDark.png'; +import ZksyncBadge from '@/assets/badges/zkSyncBadge.png'; +import ZksyncBadgeDark from '@/assets/badges/zksyncBadgeDark.png'; +import ZksyncBadgeLarge from '@/assets/badges/zksyncBadgeLarge.png'; +import ZksyncBadgeLargeDark from '@/assets/badges/zksyncBadgeLargeDark.png'; import ZoraBadge from '@/assets/badges/zoraBadge.png'; import ZoraBadgeDark from '@/assets/badges/zoraBadgeDark.png'; import ZoraBadgeLarge from '@/assets/badges/zoraBadgeLarge.png'; @@ -124,24 +123,24 @@ export default function ChainBadge({ val = isDarkMode ? BscBadgeLargeDark : BscBadgeLarge; } else if (chainId === ChainId.degen) { val = isDarkMode ? DegenBadgeLargeDark : DegenBadgeLarge; - // } else if (chainId === ChainId.gnosis) { - // val = isDarkMode ? GnosisBadgeLargeDark : GnosisBadgeLarge; - // } else if (chainId === ChainId.gravity) { - // val = isDarkMode ? GravityBadgeLargeDark : GravityBadgeLarge; + } else if (chainId === ChainId.gnosis) { + val = isDarkMode ? GnosisBadgeLargeDark : GnosisBadgeLarge; + } else if (chainId === ChainId.gravity) { + val = isDarkMode ? GravityBadgeLargeDark : GravityBadgeLarge; } else if (chainId === ChainId.ink) { val = isDarkMode ? InkBadgeLargeDark : InkBadgeLarge; - // } else if (chainId === ChainId.linea) { - // val = isDarkMode ? LineaBadgeLargeDark : LineaBadgeLarge; + } else if (chainId === ChainId.linea) { + val = isDarkMode ? LineaBadgeLargeDark : LineaBadgeLarge; } else if (chainId === ChainId.optimism) { val = isDarkMode ? OptimismBadgeLargeDark : OptimismBadgeLarge; } else if (chainId === ChainId.polygon) { val = isDarkMode ? PolygonBadgeLargeDark : PolygonBadgeLarge; - // } else if (chainId === ChainId.sanko) { - // val = isDarkMode ? SankoBadgeLargeDark : SankoBadgeLarge; - // } else if (chainId === ChainId.scroll) { - // val = isDarkMode ? ScrollBadgeLargeDark : ScrollBadgeLarge; - // } else if (chainId === ChainId.zksync) { - // val = isDarkMode ? ZksyncBadgeLargeDark : ZksyncBadgeLarge; + } else if (chainId === ChainId.sanko) { + val = isDarkMode ? SankoBadgeLargeDark : SankoBadgeLarge; + } else if (chainId === ChainId.scroll) { + val = isDarkMode ? ScrollBadgeLargeDark : ScrollBadgeLarge; + } else if (chainId === ChainId.zksync) { + val = isDarkMode ? ZksyncBadgeLargeDark : ZksyncBadgeLarge; } else if (chainId === ChainId.zora) { val = isDarkMode ? ZoraBadgeLargeDark : ZoraBadgeLarge; } @@ -160,24 +159,24 @@ export default function ChainBadge({ val = isDarkMode ? BscBadgeDark : BscBadge; } else if (chainId === ChainId.degen) { val = isDarkMode ? DegenBadgeDark : DegenBadge; - // } else if (chainId === ChainId.gnosis) { - // val = isDarkMode ? GnosisBadgeDark : GnosisBadge; - // } else if (chainId === ChainId.gravity) { - // val = isDarkMode ? GravityBadgeDark : GravityBadge; + } else if (chainId === ChainId.gnosis) { + val = isDarkMode ? GnosisBadgeDark : GnosisBadge; + } else if (chainId === ChainId.gravity) { + val = isDarkMode ? GravityBadgeDark : GravityBadge; } else if (chainId === ChainId.ink) { val = isDarkMode ? InkBadgeDark : InkBadge; - // } else if (chainId === ChainId.linea) { - // val = isDarkMode ? LineaBadgeDark : LineaBadge; + } else if (chainId === ChainId.linea) { + val = isDarkMode ? LineaBadgeDark : LineaBadge; } else if (chainId === ChainId.optimism) { val = isDarkMode ? OptimismBadgeDark : OptimismBadge; } else if (chainId === ChainId.polygon) { val = isDarkMode ? PolygonBadgeDark : PolygonBadge; - // } else if (chainId === ChainId.sanko) { - // val = isDarkMode ? SankoBadgeDark : SankoBadge; - // } else if (chainId === ChainId.scroll) { - // val = isDarkMode ? ScrollBadgeDark : ScrollBadge; - // } else if (chainId === ChainId.zksync) { - // val = isDarkMode ? ZksyncBadgeDark : ZksyncBadge; + } else if (chainId === ChainId.sanko) { + val = SankoBadge; + } else if (chainId === ChainId.scroll) { + val = isDarkMode ? ScrollBadgeDark : ScrollBadge; + } else if (chainId === ChainId.zksync) { + val = isDarkMode ? ZksyncBadgeDark : ZksyncBadge; } else if (chainId === ChainId.zora) { val = isDarkMode ? ZoraBadgeDark : ZoraBadge; } diff --git a/src/components/coin-icon/ChainImage.tsx b/src/components/coin-icon/ChainImage.tsx index 1aa5c479b8e..90f8ffbc960 100644 --- a/src/components/coin-icon/ChainImage.tsx +++ b/src/components/coin-icon/ChainImage.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, forwardRef } from 'react'; import { ChainId } from '@/state/backendNetworks/types'; import ApechainBadge from '@/assets/badges/apechain.png'; @@ -9,20 +9,31 @@ import BlastBadge from '@/assets/badges/blast.png'; import BscBadge from '@/assets/badges/bsc.png'; import DegenBadge from '@/assets/badges/degen.png'; import EthereumBadge from '@/assets/badges/ethereum.png'; -// import GnosisBadge from '@/assets/badges/gnosis.png'; -// import GravityBadge from '@/assets/badges/gravity.png'; +import GnosisBadge from '@/assets/badges/gnosis.png'; +import GravityBadge from '@/assets/badges/gravity.png'; import InkBadge from '@/assets/badges/ink.png'; -// import LineaBadge from '@/assets/badges/linea.png'; +import LineaBadge from '@/assets/badges/linea.png'; import OptimismBadge from '@/assets/badges/optimism.png'; import PolygonBadge from '@/assets/badges/polygon.png'; -// import SankoBadge from '@/assets/badges/sanko.png'; -// import ScrollBadge from '@/assets/badges/scroll.png'; -// import ZksyncBadge from '@/assets/badges/zksync.png'; +import SankoBadge from '@/assets/badges/sanko.png'; +import ScrollBadge from '@/assets/badges/scroll.png'; +import ZksyncBadge from '@/assets/badges/zksync.png'; import ZoraBadge from '@/assets/badges/zora.png'; +import FastImage, { FastImageProps, Source } from 'react-native-fast-image'; +import Animated from 'react-native-reanimated'; -import FastImage, { Source } from 'react-native-fast-image'; - -export function ChainImage({ chainId, size = 20 }: { chainId: ChainId | null | undefined; size?: number }) { +export const ChainImage = forwardRef(function ChainImage( + { + chainId, + size = 20, + style, + }: { + chainId: ChainId | null | undefined; + size?: number; + style?: FastImageProps['style']; + }, + ref +) { const source = useMemo(() => { switch (chainId) { case ChainId.apechain: @@ -39,26 +50,26 @@ export function ChainImage({ chainId, size = 20 }: { chainId: ChainId | null | u return BscBadge; case ChainId.degen: return DegenBadge; - // case ChainId.gnosis: - // return GnosisBadge; - // case ChainId.gravity: - // return GravityBadge; + case ChainId.gnosis: + return GnosisBadge; + case ChainId.gravity: + return GravityBadge; case ChainId.ink: return InkBadge; - // case ChainId.linea: - // return LineaBadge; + case ChainId.linea: + return LineaBadge; case ChainId.mainnet: return EthereumBadge; case ChainId.optimism: return OptimismBadge; case ChainId.polygon: return PolygonBadge; - // case ChainId.sanko: - // return SankoBadge; - // case ChainId.scroll: - // return ScrollBadge; - // case ChainId.zksync: - // return ZksyncBadge; + case ChainId.sanko: + return SankoBadge; + case ChainId.scroll: + return ScrollBadge; + case ChainId.zksync: + return ZksyncBadge; case ChainId.zora: return ZoraBadge; default: @@ -69,6 +80,12 @@ export function ChainImage({ chainId, size = 20 }: { chainId: ChainId | null | u if (!chainId) return null; return ( - + ); -} +}); diff --git a/src/components/fields/PasswordField.tsx b/src/components/fields/PasswordField.tsx index 0925b29862c..6d28e81e802 100644 --- a/src/components/fields/PasswordField.tsx +++ b/src/components/fields/PasswordField.tsx @@ -1,14 +1,37 @@ import React, { forwardRef, useCallback, Ref } from 'react'; -import { useTheme } from '../../theme/ThemeContext'; +import { ThemeContextProps, useTheme } from '../../theme/ThemeContext'; import { Input } from '../inputs'; import { cloudBackupPasswordMinLength } from '@/handlers/cloudBackup'; import { useDimensions } from '@/hooks'; import styled from '@/styled-thing'; -import { padding } from '@/styles'; +import { padding, position } from '@/styles'; import ShadowStack from '@/react-native-shadow-stack'; import { Box } from '@/design-system'; import { TextInput, TextInputProps, View } from 'react-native'; import { IS_IOS, IS_ANDROID } from '@/env'; +import { Icon } from '../icons'; + +const FieldAccessoryBadgeSize = 22; +const FieldAccessoryBadgeWrapper = styled(ShadowStack).attrs( + ({ theme: { colors, isDarkMode }, color }: { theme: ThemeContextProps; color: string }) => ({ + ...position.sizeAsObject(FieldAccessoryBadgeSize), + borderRadius: FieldAccessoryBadgeSize, + shadows: [[0, 4, 12, isDarkMode ? colors.shadow : color, isDarkMode ? 0.1 : 0.4]], + }) +)({ + marginBottom: 12, + position: 'absolute', + right: 12, + top: 12, +}); + +function FieldAccessoryBadge({ color, name }: { color: string; name: string }) { + return ( + + + + ); +} const Container = styled(Box)({ width: '100%', @@ -53,9 +76,9 @@ interface PasswordFieldProps extends TextInputProps { } const PasswordField = forwardRef( - ({ password, returnKeyType = 'done', style, textContentType, ...props }, ref: Ref) => { + ({ password, isInvalid, returnKeyType = 'done', style, textContentType, ...props }, ref: Ref) => { const { width: deviceWidth } = useDimensions(); - const { isDarkMode } = useTheme(); + const { isDarkMode, colors } = useTheme(); const handleFocus = useCallback(() => { if (ref && 'current' in ref && ref.current) { @@ -67,6 +90,7 @@ const PasswordField = forwardRef( + {isInvalid && } ); diff --git a/src/components/floating-emojis/FloatingEmojis.js b/src/components/floating-emojis/FloatingEmojis.js deleted file mode 100644 index f4dc8342f50..00000000000 --- a/src/components/floating-emojis/FloatingEmojis.js +++ /dev/null @@ -1,156 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Animated, View } from 'react-native'; -import FloatingEmoji from './FloatingEmoji'; -import GravityEmoji from './GravityEmoji'; -import { useTimeout } from '@/hooks'; -import { position } from '@/styles'; - -const EMPTY_ARRAY = []; -const getEmoji = emojis => Math.floor(Math.random() * emojis.length); -const getRandomNumber = (min, max) => Math.random() * (max - min) + min; - -const FloatingEmojis = ({ - centerVertically, - children, - disableHorizontalMovement, - disableRainbow, - disableVerticalMovement, - distance, - duration, - emojis, - fadeOut, - gravityEnabled, - marginTop, - opacity, - opacityThreshold, - range, - scaleTo, - setOnNewEmoji, - size, - wiggleFactor, - ...props -}) => { - const emojisArray = useMemo(() => (Array.isArray(emojis) ? emojis : [emojis]), [emojis]); - const [floatingEmojis, setEmojis] = useState(EMPTY_ARRAY); - const [startTimeout, stopTimeout] = useTimeout(); - const clearEmojis = useCallback(() => setEmojis(EMPTY_ARRAY), []); - - // 🚧️ TODO: 🚧️ - // Clear emojis if page navigatorPosition falls below 0.93 (which we should call like `pageTransitionThreshold` or something) - // otherwise, the FloatingEmojis look weird during stack transitions - - const onNewEmoji = useCallback( - (x, y) => { - // Set timeout to automatically clearEmojis after the latest one has finished animating - stopTimeout(); - startTimeout(clearEmojis, duration * 1.1); - - setEmojis(existingEmojis => { - const newEmoji = { - // if a user has smashed the button 7 times, they deserve a 🌈 rainbow - emojiToRender: - (existingEmojis.length + 1) % 7 === 0 && !disableRainbow - ? 'rainbow' - : emojisArray.length === 1 - ? emojisArray[0] - : emojisArray[getEmoji(emojisArray)], - x: x ? x - getRandomNumber(-20, 20) : getRandomNumber(...range), - y: y || 0, - }; - return [...existingEmojis, newEmoji]; - }); - }, - [clearEmojis, disableRainbow, duration, emojisArray, range, startTimeout, stopTimeout] - ); - - useEffect(() => { - setOnNewEmoji?.(onNewEmoji); - return () => setOnNewEmoji?.(undefined); - }, [setOnNewEmoji, onNewEmoji]); - - return ( - - {typeof children === 'function' ? children({ onNewEmoji }) : children} - - {gravityEnabled - ? floatingEmojis.map(({ emojiToRender, x, y }, index) => ( - - )) - : floatingEmojis.map(({ emojiToRender, x, y }, index) => ( - - ))} - - - ); -}; - -FloatingEmojis.propTypes = { - centerVertically: PropTypes.bool, - children: PropTypes.node, - disableHorizontalMovement: PropTypes.bool, - disableRainbow: PropTypes.bool, - disableVerticalMovement: PropTypes.bool, - distance: PropTypes.number, - duration: PropTypes.number, - emojis: PropTypes.arrayOf(PropTypes.string).isRequired, - fadeOut: PropTypes.bool, - gravityEnabled: PropTypes.bool, - marginTop: PropTypes.number, - opacity: PropTypes.oneOfType([PropTypes.number, PropTypes.object]), - opacityThreshold: PropTypes.number, - range: PropTypes.arrayOf(PropTypes.number), - scaleTo: PropTypes.number, - setOnNewEmoji: PropTypes.func, - size: PropTypes.string.isRequired, - wiggleFactor: PropTypes.number, -}; - -FloatingEmojis.defaultProps = { - distance: 130, - duration: 2000, - // Defaults the emoji to 👍️ (thumbs up). - // To view complete list of emojis compatible with this component, - // head to https://github.com/muan/unicode-emoji-json/blob/master/data-by-emoji.json - emojis: ['thumbs_up'], - fadeOut: true, - opacity: 1, - range: [0, 80], - scaleTo: 1, - size: 30, - wiggleFactor: 0.5, -}; - -export default FloatingEmojis; diff --git a/src/components/floating-emojis/FloatingEmojis.tsx b/src/components/floating-emojis/FloatingEmojis.tsx new file mode 100644 index 00000000000..7eccc8b69de --- /dev/null +++ b/src/components/floating-emojis/FloatingEmojis.tsx @@ -0,0 +1,151 @@ +import React, { useCallback, useEffect, useMemo, useState, ReactNode } from 'react'; +import { Animated, View, ViewProps } from 'react-native'; +import FloatingEmoji from './FloatingEmoji'; +import GravityEmoji from './GravityEmoji'; +import { useTimeout } from '@/hooks'; +import { position } from '@/styles'; +import { DebugLayout } from '@/design-system'; +import { DEVICE_HEIGHT, DEVICE_WIDTH } from '@/utils/deviceUtils'; +import { AbsolutePortal } from '../AbsolutePortal'; + +interface Emoji { + emojiToRender: string; + x: number; + y: number; +} + +interface FloatingEmojisProps extends Omit { + centerVertically?: boolean; + children?: ReactNode | ((props: { onNewEmoji: (x?: number, y?: number) => void }) => ReactNode); + disableHorizontalMovement?: boolean; + disableRainbow?: boolean; + disableVerticalMovement?: boolean; + distance?: number; + duration?: number; + emojis: string[]; + fadeOut?: boolean; + gravityEnabled?: boolean; + marginTop?: number; + opacity?: number | Animated.AnimatedInterpolation; + opacityThreshold?: number; + range?: [number, number]; + scaleTo?: number; + setOnNewEmoji?: (fn: ((x?: number, y?: number) => void) | undefined) => void; + size: number; + wiggleFactor?: number; +} + +const EMPTY_ARRAY: Emoji[] = []; +const getEmoji = (emojis: string[]) => Math.floor(Math.random() * emojis.length); +const getRandomNumber = (min: number, max: number) => Math.random() * (max - min) + min; + +const FloatingEmojis: React.FC = ({ + centerVertically, + children, + disableHorizontalMovement, + disableRainbow, + disableVerticalMovement, + distance = 130, + duration = 2000, + emojis, + fadeOut = true, + gravityEnabled, + marginTop, + opacity = 1, + opacityThreshold, + range: [rangeMin, rangeMax] = [0, 80], + scaleTo = 1, + setOnNewEmoji, + size = 30, + wiggleFactor = 0.5, + style, + ...props +}) => { + const emojisArray = useMemo(() => (Array.isArray(emojis) ? emojis : [emojis]), [emojis]); + const [floatingEmojis, setEmojis] = useState(EMPTY_ARRAY); + const [startTimeout, stopTimeout] = useTimeout(); + const clearEmojis = useCallback(() => setEmojis(EMPTY_ARRAY), []); + + // 🚧️ TODO: 🚧️ + // Clear emojis if page navigatorPosition falls below 0.93 (which we should call like `pageTransitionThreshold` or something) + // otherwise, the FloatingEmojis look weird during stack transitions + + const onNewEmoji = useCallback( + (x?: number, y?: number) => { + // Set timeout to automatically clearEmojis after the latest one has finished animating + stopTimeout(); + startTimeout(clearEmojis, duration * 1.1); + + setEmojis(existingEmojis => { + const newEmoji = { + emojiToRender: + (existingEmojis.length + 1) % 7 === 0 && !disableRainbow + ? 'rainbow' + : emojisArray.length === 1 + ? emojisArray[0] + : emojisArray[getEmoji(emojisArray)], + x: x !== undefined ? x - getRandomNumber(-20, 20) : getRandomNumber(rangeMin, rangeMax), + y: y || 0, + }; + return [...existingEmojis, newEmoji]; + }); + }, + [clearEmojis, disableRainbow, duration, emojisArray, rangeMin, rangeMax, startTimeout, stopTimeout] + ); + + useEffect(() => { + setOnNewEmoji?.(onNewEmoji); + return () => setOnNewEmoji?.(undefined); + }, [setOnNewEmoji, onNewEmoji]); + + return ( + + {typeof children === 'function' ? children({ onNewEmoji }) : children} + + + {gravityEnabled + ? floatingEmojis.map(({ emojiToRender, x, y }, index) => ( + + )) + : floatingEmojis.map(({ emojiToRender, x, y }, index) => ( + + ))} + + + + ); +}; + +export default FloatingEmojis; diff --git a/src/components/floating-emojis/GravityEmoji.tsx b/src/components/floating-emojis/GravityEmoji.tsx index 2bf06a3901f..0b1de95b47c 100644 --- a/src/components/floating-emojis/GravityEmoji.tsx +++ b/src/components/floating-emojis/GravityEmoji.tsx @@ -4,7 +4,9 @@ import { Emoji } from '../text'; interface GravityEmojiProps { distance: number; + duration: number; emoji: string; + index: number; left: number; size: number; top: number; diff --git a/src/components/info-alert/info-alert.tsx b/src/components/info-alert/info-alert.tsx index bb17325be89..dbfa3aef5f1 100644 --- a/src/components/info-alert/info-alert.tsx +++ b/src/components/info-alert/info-alert.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Box, Text, useForegroundColor } from '@/design-system'; +import { Box, Text } from '@/design-system'; type InfoAlertProps = { title: string; diff --git a/src/components/remote-promo-sheet/runChecks.ts b/src/components/remote-promo-sheet/runChecks.ts index f83170eecce..9167de41182 100644 --- a/src/components/remote-promo-sheet/runChecks.ts +++ b/src/components/remote-promo-sheet/runChecks.ts @@ -1,7 +1,6 @@ import { IS_TEST } from '@/env'; -import { runFeatureUnlockChecks } from '@/handlers/walletReadyEvents'; +import { runFeaturesLocalCampaignAndBackupChecks } from '@/handlers/walletReadyEvents'; import { logger } from '@/logger'; -import { runLocalCampaignChecks } from '@/components/remote-promo-sheet/localCampaignChecks'; import { checkForRemotePromoSheet } from '@/components/remote-promo-sheet/checkForRemotePromoSheet'; import { useCallback, useEffect } from 'react'; import { InteractionManager } from 'react-native'; @@ -19,11 +18,7 @@ export const useRunChecks = ({ runChecksOnMount = true, walletReady }: { runChec return; } - const showedFeatureUnlock = await runFeatureUnlockChecks(); - if (showedFeatureUnlock) return; - - const showedLocalPromo = await runLocalCampaignChecks(); - if (showedLocalPromo) return; + if (await runFeaturesLocalCampaignAndBackupChecks()) return; if (!remotePromoSheets) { logger.debug('[useRunChecks]: remote promo sheets is disabled'); diff --git a/src/components/secret-display/SecretDisplaySection.tsx b/src/components/secret-display/SecretDisplaySection.tsx index 0ef93ba05e6..3cd37f05611 100644 --- a/src/components/secret-display/SecretDisplaySection.tsx +++ b/src/components/secret-display/SecretDisplaySection.tsx @@ -1,5 +1,4 @@ import { RouteProp, useRoute } from '@react-navigation/native'; -import { captureException } from '@sentry/react-native'; import React, { ReactNode, useCallback, useEffect, useState } from 'react'; import { createdWithBiometricError, identifyWalletType, loadPrivateKey, loadSeedPhraseAndMigrateIfNeeded } from '@/model/wallet'; import ActivityIndicator from '../ActivityIndicator'; @@ -25,6 +24,7 @@ import { useNavigation } from '@/navigation'; import { ImgixImage } from '../images'; import RoutesWithPlatformDifferences from '@/navigation/routesNames'; import { Source } from 'react-native-fast-image'; +import { backupsStore } from '@/state/backups/backups'; const MIN_HEIGHT = 740; @@ -63,6 +63,9 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }: const { colors } = useTheme(); const { params } = useRoute>(); const { selectedWallet, wallets } = useWallets(); + const { backupProvider } = backupsStore(state => ({ + backupProvider: state.backupProvider, + })); const { onManuallyBackupWalletId } = useWalletManualBackup(); const { navigate } = useNavigation(); @@ -124,9 +127,12 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }: const handleConfirmSaved = useCallback(() => { if (backupType === WalletBackupTypes.manual) { onManuallyBackupWalletId(walletId); + if (!backupProvider) { + backupsStore.getState().setBackupProvider(WalletBackupTypes.manual); + } navigate(RoutesWithPlatformDifferences.SETTINGS_SECTION_BACKUP); } - }, [backupType, walletId, onManuallyBackupWalletId, navigate]); + }, [backupType, onManuallyBackupWalletId, walletId, backupProvider, navigate]); const getIconForBackupType = useCallback(() => { if (isBackingUp) { diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index 9a7ea341748..81f82de844b 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -31,7 +31,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, weight = const goToSwap = useCallback(async () => { const chainsIdByName = useBackendNetworksStore.getState().getChainsIdByName(); const chainsName = useBackendNetworksStore.getState().getChainsName(); - const chainId = chainsIdByName[asset.network]; + const chainId = asset.chainId || chainsIdByName[asset.network]; const uniqueId = `${asset.address}_${chainId}`; const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); diff --git a/src/config/experimental.ts b/src/config/experimental.ts index b68e23260ff..cef7e04d7d7 100644 --- a/src/config/experimental.ts +++ b/src/config/experimental.ts @@ -29,6 +29,7 @@ export const DEGEN_MODE = 'Degen Mode'; export const FEATURED_RESULTS = 'Featured Results'; export const CLAIMABLES = 'Claimables'; export const NFTS_ENABLED = 'Nfts Enabled'; +export const TRENDING_TOKENS = 'Trending Tokens'; /** * A developer setting that pushes log lines to an array in-memory so that @@ -66,6 +67,7 @@ export const defaultConfig: Record = { [FEATURED_RESULTS]: { settings: true, value: false }, [CLAIMABLES]: { settings: true, value: false }, [NFTS_ENABLED]: { settings: true, value: !!IS_TEST }, + [TRENDING_TOKENS]: { settings: true, value: false }, }; export const defaultConfigValues: Record = Object.fromEntries( diff --git a/src/design-system/components/Inline/Inline.tsx b/src/design-system/components/Inline/Inline.tsx index 5754bae6a93..3f93791cbc8 100644 --- a/src/design-system/components/Inline/Inline.tsx +++ b/src/design-system/components/Inline/Inline.tsx @@ -51,7 +51,7 @@ export function Inline({ > {wrap || !separator ? children - : Children.map(children, (child, index) => { + : Children.toArray(children).map((child, index) => { if (!child) return null; return ( <> diff --git a/src/graphql/queries/arc.graphql b/src/graphql/queries/arc.graphql index 68e797864e5..17dccca539c 100644 --- a/src/graphql/queries/arc.graphql +++ b/src/graphql/queries/arc.graphql @@ -522,6 +522,7 @@ query trendingTokens( $sortBy: TrendingSort $sortDirection: SortDirection $walletAddress: String + $limit: Int ) { trendingTokens( chainId: $chainId @@ -531,6 +532,7 @@ query trendingTokens( sortBy: $sortBy sortDirection: $sortDirection walletAddress: $walletAddress + limit: $limit ) { data { colors { diff --git a/src/handlers/cloudBackup.ts b/src/handlers/cloudBackup.ts index 1eb3f5be795..14347c42a75 100644 --- a/src/handlers/cloudBackup.ts +++ b/src/handlers/cloudBackup.ts @@ -1,14 +1,15 @@ import { sortBy } from 'lodash'; // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'reac... Remove this comment to see the full error message import RNCloudFs from 'react-native-cloud-fs'; -import { RAINBOW_MASTER_KEY } from 'react-native-dotenv'; import RNFS from 'react-native-fs'; import AesEncryptor from '../handlers/aesEncryption'; import { logger, RainbowError } from '@/logger'; import { IS_ANDROID, IS_IOS } from '@/env'; -import { CloudBackups } from '@/model/backup'; +import { BackupFile, CloudBackups } from '@/model/backup'; + const REMOTE_BACKUP_WALLET_DIR = 'rainbow.me/wallet-backups'; -const USERDATA_FILE = 'UserData.json'; +export const USERDATA_FILE = 'UserData.json'; + const encryptor = new AesEncryptor(); export const CLOUD_BACKUP_ERRORS = { @@ -65,13 +66,18 @@ export async function fetchAllBackups(): Promise { if (android) { await RNCloudFs.loginIfNeeded(); } - return RNCloudFs.listFiles({ + + const files = await RNCloudFs.listFiles({ scope: 'hidden', targetPath: REMOTE_BACKUP_WALLET_DIR, }); + + return { + files: files?.files?.filter((file: BackupFile) => normalizeAndroidBackupFilename(file.name) !== USERDATA_FILE) || [], + }; } -export async function encryptAndSaveDataToCloud(data: any, password: any, filename: any) { +export async function encryptAndSaveDataToCloud(data: Record, password: string, filename: string) { // Encrypt the data try { const encryptedData = await encryptor.encrypt(password, JSON.stringify(data)); @@ -100,6 +106,7 @@ export async function encryptAndSaveDataToCloud(data: any, password: any, filena scope, sourcePath: sourceUri, targetPath: destinationPath, + update: true, }); // Now we need to verify the file has been stored in the cloud const exists = await RNCloudFs.fileExists( @@ -201,19 +208,6 @@ export async function getDataFromCloud(backupPassword: any, filename: string | n throw error; } -export async function backupUserDataIntoCloud(data: any) { - const filename = USERDATA_FILE; - const password = RAINBOW_MASTER_KEY; - return encryptAndSaveDataToCloud(data, password, filename); -} - -export async function fetchUserDataFromCloud() { - const filename = USERDATA_FILE; - const password = RAINBOW_MASTER_KEY; - - return getDataFromCloud(password, filename); -} - export const cloudBackupPasswordMinLength = 8; export function isCloudBackupPasswordValid(password: any) { diff --git a/src/handlers/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts index 1cfa62be144..b62749da519 100644 --- a/src/handlers/walletReadyEvents.ts +++ b/src/handlers/walletReadyEvents.ts @@ -1,4 +1,3 @@ -import { IS_TESTING } from 'react-native-dotenv'; import { triggerOnSwipeLayout } from '../navigation/onNavigationStateChange'; import { getKeychainIntegrityState } from './localstorage/globalSettings'; import { runLocalCampaignChecks } from '@/components/remote-promo-sheet/localCampaignChecks'; @@ -6,18 +5,15 @@ import { EthereumAddress } from '@/entities'; import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import WalletTypes from '@/helpers/walletTypes'; import { featureUnlockChecks } from '@/featuresToUnlock'; -import { AllRainbowWallets, RainbowAccount, RainbowWallet } from '@/model/wallet'; +import { AllRainbowWallets, RainbowAccount } from '@/model/wallet'; import { Navigation } from '@/navigation'; import store from '@/redux/store'; import { checkKeychainIntegrity } from '@/redux/wallets'; import Routes from '@/navigation/routesNames'; import { logger } from '@/logger'; -import { checkWalletsForBackupStatus } from '@/screens/SettingsSheet/utils'; -import walletBackupTypes from '@/helpers/walletBackupTypes'; -import { InteractionManager } from 'react-native'; - -const BACKUP_SHEET_DELAY_MS = 3000; +import { IS_TEST } from '@/env'; +import { backupsStore, LoadingStates } from '@/state/backups/backups'; export const runKeychainIntegrityChecks = async () => { const keychainIntegrityState = await getKeychainIntegrityState(); @@ -26,60 +22,38 @@ export const runKeychainIntegrityChecks = async () => { } }; -export const runWalletBackupStatusChecks = () => { - const { - selected, - wallets, - }: { - wallets: AllRainbowWallets | null; - selected: RainbowWallet | undefined; - } = store.getState().wallets; - - // count how many visible, non-imported and non-readonly wallets are not backed up - if (!wallets) return; - - const { backupProvider } = checkWalletsForBackupStatus(wallets); - - const rainbowWalletsNotBackedUp = Object.values(wallets).filter(wallet => { - const hasVisibleAccount = wallet.addresses?.find((account: RainbowAccount) => account.visible); - return ( - !wallet.imported && - !!hasVisibleAccount && - wallet.type !== WalletTypes.readOnly && - wallet.type !== WalletTypes.bluetooth && - !wallet.backedUp - ); +const delay = (ms: number) => + new Promise(resolve => { + setTimeout(resolve, ms); }); - if (!rainbowWalletsNotBackedUp.length) return; - - logger.debug('[walletReadyEvents]: there is a rainbow wallet not backed up'); +const promptForBackupOnceReadyOrNotAvailable = async (): Promise => { + const { status } = backupsStore.getState(); + if (LoadingStates.includes(status)) { + await delay(1000); + return promptForBackupOnceReadyOrNotAvailable(); + } - const hasSelectedWallet = rainbowWalletsNotBackedUp.find(notBackedUpWallet => notBackedUpWallet.id === selected!.id); - logger.debug('[walletReadyEvents]: rainbow wallet not backed up that is selected?', { - hasSelectedWallet, - }); + logger.debug(`[walletReadyEvents]: BackupSheet: showing backup now sheet for selected wallet`); + triggerOnSwipeLayout(() => + Navigation.handleAction(Routes.BACKUP_SHEET, { + step: WalletBackupStepTypes.backup_prompt, + }) + ); + return true; +}; - // if one of them is selected, show the default BackupSheet - if (selected && hasSelectedWallet && IS_TESTING !== 'true') { - let stepType: string = WalletBackupStepTypes.no_provider; - if (backupProvider === walletBackupTypes.cloud) { - stepType = WalletBackupStepTypes.backup_now_to_cloud; - } else if (backupProvider === walletBackupTypes.manual) { - stepType = WalletBackupStepTypes.backup_now_manually; - } +export const runWalletBackupStatusChecks = async (): Promise => { + const { selected } = store.getState().wallets; + if (!selected || IS_TEST) return false; - setTimeout(() => { - logger.debug(`[walletReadyEvents]: showing ${stepType} backup sheet for selected wallet`); - triggerOnSwipeLayout(() => - Navigation.handleAction(Routes.BACKUP_SHEET, { - step: stepType, - }) - ); - }, BACKUP_SHEET_DELAY_MS); - return; + const selectedWalletNeedsBackedUp = + !selected.backedUp && !selected.damaged && selected.type !== WalletTypes.readOnly && selected.type !== WalletTypes.bluetooth; + if (selectedWalletNeedsBackedUp) { + logger.debug('[walletReadyEvents]: Selected wallet is not backed up, prompting backup sheet'); + return promptForBackupOnceReadyOrNotAvailable(); } - return; + return false; }; export const runFeatureUnlockChecks = async (): Promise => { @@ -107,19 +81,24 @@ export const runFeatureUnlockChecks = async (): Promise => { // short circuits once the first feature is unlocked for (const featureUnlockCheck of featureUnlockChecks) { - InteractionManager.runAfterInteractions(async () => { - const unlockNow = await featureUnlockCheck(walletsToCheck); - if (unlockNow) { - return true; - } - }); + const unlockNow = await featureUnlockCheck(walletsToCheck); + if (unlockNow) { + return true; + } } return false; }; -export const runFeatureAndLocalCampaignChecks = async () => { - const showingFeatureUnlock: boolean = await runFeatureUnlockChecks(); - if (!showingFeatureUnlock) { - await runLocalCampaignChecks(); +export const runFeaturesLocalCampaignAndBackupChecks = async () => { + if (await runFeatureUnlockChecks()) { + return true; } + if (await runLocalCampaignChecks()) { + return true; + } + if (await runWalletBackupStatusChecks()) { + return true; + } + + return false; }; diff --git a/src/helpers/strings.ts b/src/helpers/strings.ts index 4e7fd76ab91..b53bb781d40 100644 --- a/src/helpers/strings.ts +++ b/src/helpers/strings.ts @@ -1,4 +1,8 @@ +import store from '@/redux/store'; import { memoFn } from '../utils/memoFn'; +import { supportedNativeCurrencies } from '@/references'; +import { NativeCurrencyKey } from '@/entities'; +import { convertAmountToNativeDisplayWorklet } from './utilities'; /** * @desc subtracts two numbers * @param {String} str @@ -10,3 +14,128 @@ export const containsEmoji = memoFn(str => { // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'. return !!str.match(ranges.join('|')); }); + +/* + * Return the given number as a formatted string. The default format is a plain + * integer with thousands-separator commas. The optional parameters facilitate + * other formats: + * - decimals = the number of decimals places to round to and show + * - valueIfNaN = the value to show for non-numeric input + * - style + * - '%': multiplies by 100 and appends a percent symbol + * - '$': prepends a dollar sign + * - useOrderSuffix = whether to use suffixes like k for 1,000, etc. + * - orderSuffixes = the list of suffixes to use + * - minOrder and maxOrder allow the order to be constrained. Examples: + * - minOrder = 1 means the k suffix should be used for numbers < 1,000 + * - maxOrder = 1 means the k suffix should be used for numbers >= 1,000,000 + */ +export function formatNumber( + number: string | number, + { + decimals = 0, + valueIfNaN = '', + style = '', + useOrderSuffix = false, + orderSuffixes = ['', 'K', 'M', 'B', 'T'], + minOrder = 0, + maxOrder = Infinity, + } = {} +) { + let x = parseFloat(`${number}`); + + if (isNaN(x)) return valueIfNaN; + + if (style === '%') x *= 100.0; + + let order; + if (!isFinite(x) || !useOrderSuffix) order = 0; + else if (minOrder === maxOrder) order = minOrder; + else { + const unboundedOrder = Math.floor(Math.log10(Math.abs(x)) / 3); + order = Math.max(0, minOrder, Math.min(unboundedOrder, maxOrder, orderSuffixes.length - 1)); + } + + const orderSuffix = orderSuffixes[order]; + if (order !== 0) x /= Math.pow(10, order * 3); + + return ( + (style === '$' ? '$' : '') + + x.toLocaleString('en-US', { + style: 'decimal', + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }) + + orderSuffix + + (style === '%' ? '%' : '') + ); +} + +type CurrencyFormatterOptions = { + decimals?: number; + valueIfNaN?: string; + currency?: NativeCurrencyKey; +}; + +const toSubscript = (str: string | number) => str.toString().replace(/[0-9]/g, num => String.fromCharCode(0x2080 + +num)); + +/* + converts 6.9e-7 to 0.00000069 +*/ +const toDecimalString = (num: number): string => { + const [coefficient, exponent] = num.toExponential(20).split('e'); + const exp = parseInt(exponent); + const digits = coefficient.replace('.', '').replace(/0+$/, ''); + + if (exp >= 0) { + const position = exp + 1; + if (position >= digits.length) return digits + '0'.repeat(position - digits.length); + return digits.slice(0, position) + (digits.slice(position) && '.' + digits.slice(position)); + } + return '0.' + '0'.repeat(Math.abs(exp) - 1) + digits; +}; + +/* + formats a numeric string like 0000069 to 0₅69 +*/ +function formatFraction(fraction: string) { + const leadingZeros = fraction.match(/^[0]+/)?.[0].length || 0; + if (+fraction === 0) return '00'; + + const significantDigits = fraction.slice(leadingZeros, leadingZeros + 2); + if (+significantDigits === 0) return '00'; + + if (leadingZeros >= 4) return `0${toSubscript(leadingZeros)}${significantDigits}`; + return `${'0'.repeat(leadingZeros)}${significantDigits}`; +} + +export function formatCurrency( + value: string | number, + { valueIfNaN = '', currency = store.getState().settings.nativeCurrency }: CurrencyFormatterOptions = {} +): string { + const numericString = typeof value === 'number' ? toDecimalString(value) : String(value); + if (isNaN(+numericString)) return valueIfNaN; + + const currencySymbol = supportedNativeCurrencies[currency].symbol; + const [whole, fraction = ''] = numericString.split('.'); + + const numericalWholeNumber = +whole; + if (numericalWholeNumber > 0) { + // if the fraction is empty and the numeric string is less than 6 characters, we can just run it through our native currency display worklet + if (whole.length <= 6) { + return convertAmountToNativeDisplayWorklet(numericString, currency, false, true); + } + + const decimals = supportedNativeCurrencies[currency].decimals; + // otherwise for > 6 figs native value we need to format in compact notation + const formattedWhole = formatNumber(numericString, { decimals, useOrderSuffix: true }); + return `${currencySymbol}${formattedWhole}`; + } + + const formattedWhole = formatNumber(whole, { decimals: 0, useOrderSuffix: true }); + const formattedFraction = formatFraction(fraction); + // if it ends with a non-numeric character, it's in compact notation like '1.2K' + if (isNaN(+formattedWhole[formattedWhole.length - 1])) return `${currencySymbol}${formattedWhole}`; + + return `${currencySymbol}${formattedWhole}.${formattedFraction}`; +} diff --git a/src/helpers/walletBackupStepTypes.ts b/src/helpers/walletBackupStepTypes.ts index d3afc9598a2..2fbf0cb8f9e 100644 --- a/src/helpers/walletBackupStepTypes.ts +++ b/src/helpers/walletBackupStepTypes.ts @@ -1,5 +1,5 @@ export default { - no_provider: 'no_provider', + backup_prompt: 'backup_prompt', backup_manual: 'backup_manual', backup_cloud: 'backup_cloud', restore_from_backup: 'restore_from_backup', diff --git a/src/helpers/walletLoadingStates.ts b/src/helpers/walletLoadingStates.ts new file mode 100644 index 00000000000..a9cdd674d2e --- /dev/null +++ b/src/helpers/walletLoadingStates.ts @@ -0,0 +1,10 @@ +import * as i18n from '@/languages'; + +export const WalletLoadingStates = { + BACKING_UP_WALLET: i18n.t('loading.backing_up'), + CREATING_WALLET: i18n.t('loading.creating_wallet'), + IMPORTING_WALLET: i18n.t('loading.importing_wallet'), + RESTORING_WALLET: i18n.t('loading.restoring'), +} as const; + +export type WalletLoadingStates = (typeof WalletLoadingStates)[keyof typeof WalletLoadingStates]; diff --git a/src/hooks/reanimated/useSyncSharedValue.ts b/src/hooks/reanimated/useSyncSharedValue.ts index f8c19c71a0c..c48f83a3643 100644 --- a/src/hooks/reanimated/useSyncSharedValue.ts +++ b/src/hooks/reanimated/useSyncSharedValue.ts @@ -9,14 +9,14 @@ interface BaseSyncParams { /** A boolean or shared value boolean that controls whether synchronization is paused. */ pauseSync?: DerivedValue | SharedValue | boolean; /** The JS state to be synchronized. */ - state: T | undefined; + state: T; } interface SharedToStateParams extends BaseSyncParams { /** The setter function for the JS state (only applicable when `syncDirection` is `'sharedValueToState'`). */ setState: (value: T) => void; /** The shared value to be synchronized. */ - sharedValue: DerivedValue | DerivedValue | SharedValue | SharedValue; + sharedValue: DerivedValue | SharedValue; /** The direction of synchronization. */ syncDirection: 'sharedValueToState'; } @@ -24,7 +24,7 @@ interface SharedToStateParams extends BaseSyncParams { interface StateToSharedParams extends BaseSyncParams { setState?: never; /** The shared value to be synchronized. */ - sharedValue: SharedValue | SharedValue; + sharedValue: SharedValue; /** The direction of synchronization. */ syncDirection: 'stateToSharedValue'; } @@ -73,7 +73,7 @@ export function useSyncSharedValue({ compareDepth = 'deep', pauseSync, setSta }, shouldSync => { if (shouldSync) { - if (syncDirection === 'sharedValueToState' && sharedValue.value !== undefined) { + if (syncDirection === 'sharedValueToState') { runOnJS(setState)(sharedValue.value); } else if (syncDirection === 'stateToSharedValue') { sharedValue.value = state; diff --git a/src/hooks/useAccountENSDomains.ts b/src/hooks/useAccountENSDomains.ts index c9923dfa606..f50aaef778d 100644 --- a/src/hooks/useAccountENSDomains.ts +++ b/src/hooks/useAccountENSDomains.ts @@ -14,7 +14,7 @@ const STALE_TIME = 10000; async function fetchAccountENSDomains({ accountAddress }: { accountAddress: string }) { const result = await fetchAccountDomains(accountAddress); - if (!result.account) return []; + if (!result?.account) return []; const { domains: controlledDomains, registrations } = result.account; const registarDomains = registrations?.map(({ domain }) => domain); diff --git a/src/hooks/useActiveRoute.ts b/src/hooks/useActiveRoute.ts new file mode 100644 index 00000000000..523eb741004 --- /dev/null +++ b/src/hooks/useActiveRoute.ts @@ -0,0 +1,16 @@ +import { Navigation, useNavigation } from '@/navigation'; +import { useEffect, useState } from 'react'; + +export const useActiveRoute = () => { + const { addListener } = useNavigation(); + const [activeRoute, setActiveRoute] = useState(Navigation.getActiveRoute()); + + useEffect(() => { + const unsubscribe = addListener('state', () => { + setActiveRoute(Navigation.getActiveRoute()); + }); + return unsubscribe; + }, [addListener]); + + return activeRoute?.name; +}; diff --git a/src/hooks/useCloudBackups.ts b/src/hooks/useCloudBackups.ts deleted file mode 100644 index 506e669c682..00000000000 --- a/src/hooks/useCloudBackups.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { useEffect, useState } from 'react'; -import type { BackupUserData, CloudBackups } from '../model/backup'; -import { fetchAllBackups, fetchUserDataFromCloud, isCloudBackupAvailable, syncCloud } from '@/handlers/cloudBackup'; -import { RainbowError, logger } from '@/logger'; - -export const enum CloudBackupStep { - IDLE, - SYNCING, - FETCHING_USER_DATA, - FETCHING_ALL_BACKUPS, - FAILED, -} - -export default function useCloudBackups() { - const [isFetching, setIsFetching] = useState(false); - const [backups, setBackups] = useState({ - files: [], - }); - - const [step, setStep] = useState(CloudBackupStep.SYNCING); - - const [userData, setUserData] = useState(); - - const fetchBackups = async () => { - try { - setIsFetching(true); - const isAvailable = isCloudBackupAvailable(); - if (!isAvailable) { - logger.debug('[useCloudBackups]: Cloud backup is not available'); - setIsFetching(false); - setStep(CloudBackupStep.IDLE); - return; - } - - setStep(CloudBackupStep.SYNCING); - logger.debug('[useCloudBackups]: Syncing with cloud'); - await syncCloud(); - - setStep(CloudBackupStep.FETCHING_USER_DATA); - logger.debug('[useCloudBackups]: Fetching user data'); - const userData = await fetchUserDataFromCloud(); - setUserData(userData); - - setStep(CloudBackupStep.FETCHING_ALL_BACKUPS); - logger.debug('[useCloudBackups]: Fetching all backups'); - const backups = await fetchAllBackups(); - - logger.debug(`[useCloudBackups]: Retrieved ${backups.files.length} backup files`); - setBackups(backups); - setStep(CloudBackupStep.IDLE); - } catch (e) { - setStep(CloudBackupStep.FAILED); - logger.error(new RainbowError('[useCloudBackups]: Failed to fetch all backups'), { - error: e, - }); - } - setIsFetching(false); - }; - - useEffect(() => { - fetchBackups(); - }, []); - - return { - isFetching, - backups, - fetchBackups, - userData, - step, - }; -} diff --git a/src/hooks/useFarcasterAccountForWallets.ts b/src/hooks/useFarcasterAccountForWallets.ts index 4c5702051b7..80873b867e3 100644 --- a/src/hooks/useFarcasterAccountForWallets.ts +++ b/src/hooks/useFarcasterAccountForWallets.ts @@ -12,12 +12,12 @@ import { AllRainbowWallets } from '@/model/wallet'; type SummaryData = ReturnType['data']; -const getWalletForAddress = (wallets: AllRainbowWallets, address: string) => { +const getWalletForAddress = (wallets: AllRainbowWallets | null, address: string) => { return Object.values(wallets || {}).find(wallet => wallet.addresses.some(addr => isLowerCaseMatch(addr.address, address))); }; -export const useFarcasterWalletAddress = () => { - const [farcasterWalletAddress, setFarcasterWalletAddress] = useState(null); +export const useFarcasterAccountForWallets = () => { + const [farcasterWalletAddress, setFarcasterWalletAddress] = useState
(); const { accountAddress } = useAccountSettings(); const { wallets } = useWallets(); @@ -33,31 +33,32 @@ export const useFarcasterWalletAddress = () => { currency: store.getState().settings.nativeCurrency, }) ); - if (isEmpty(summaryData?.data.addresses) || isEmpty(wallets)) { - setFarcasterWalletAddress(null); + const addresses = summaryData?.data.addresses; + + if (!addresses || isEmpty(addresses) || isEmpty(wallets)) { + setFarcasterWalletAddress(undefined); return; } - const selectedAddressFid = summaryData?.data.addresses[accountAddress as Address]?.meta?.farcaster?.fid; - - if (selectedAddressFid && getWalletForAddress(wallets || {}, accountAddress)?.type !== walletTypes.readOnly) { + const selectedAddressFid = addresses[accountAddress]?.meta?.farcaster?.fid; + if (selectedAddressFid && getWalletForAddress(wallets, accountAddress)?.type !== walletTypes.readOnly) { setFarcasterWalletAddress(accountAddress); return; } - const farcasterWalletAddress = Object.keys(summaryData?.data.addresses || {}).find(addr => { + const farcasterWalletAddress = Object.keys(addresses).find(addr => { const address = addr as Address; const faracsterId = summaryData?.data.addresses[address]?.meta?.farcaster?.fid; - if (faracsterId && getWalletForAddress(wallets || {}, address)?.type !== walletTypes.readOnly) { - return faracsterId; + if (faracsterId && getWalletForAddress(wallets, address)?.type !== walletTypes.readOnly) { + return address; } - }); + }) as Address | undefined; if (farcasterWalletAddress) { setFarcasterWalletAddress(farcasterWalletAddress); return; } - setFarcasterWalletAddress(null); + setFarcasterWalletAddress(undefined); }, [wallets, allAddresses, accountAddress]); return farcasterWalletAddress; diff --git a/src/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts index d0f92e12a3a..f1cde6313da 100644 --- a/src/hooks/useImportingWallet.ts +++ b/src/hooks/useImportingWallet.ts @@ -3,7 +3,6 @@ import lang from 'i18n-js'; import { keys } from 'lodash'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { InteractionManager, Keyboard, TextInput } from 'react-native'; -import { IS_TESTING } from 'react-native-dotenv'; import { useDispatch } from 'react-redux'; import useAccountSettings from './useAccountSettings'; import { fetchENSAvatar } from './useENSAvatar'; @@ -29,9 +28,9 @@ import { deriveAccountFromWalletInput } from '@/utils/wallet'; import { logger, RainbowError } from '@/logger'; import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; -import { checkWalletsForBackupStatus } from '@/screens/SettingsSheet/utils'; -import walletBackupTypes from '@/helpers/walletBackupTypes'; import { ChainId } from '@/state/backendNetworks/types'; +import { backupsStore } from '@/state/backups/backups'; +import { IS_TEST } from '@/env'; export default function useImportingWallet({ showImportModal = true } = {}) { const { accountAddress } = useAccountSettings(); @@ -52,6 +51,10 @@ export default function useImportingWallet({ showImportModal = true } = {}) { const { updateWalletENSAvatars } = useWalletENSAvatar(); const profilesEnabled = useExperimentalFlag(PROFILES); + const { backupProvider } = backupsStore(state => ({ + backupProvider: state.backupProvider, + })); + const inputRef = useRef(null); const { handleFocus } = useMagicAutofocus(inputRef); @@ -291,7 +294,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) { image, true ); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); handleSetImporting(false); } else { const previousWalletCount = keys(wallets).length; @@ -346,19 +349,11 @@ export default function useImportingWallet({ showImportModal = true } = {}) { isValidBluetoothDeviceId(input) ) ) { - const { backupProvider } = checkWalletsForBackupStatus(wallets); - - let stepType: string = WalletBackupStepTypes.no_provider; - if (backupProvider === walletBackupTypes.cloud) { - stepType = WalletBackupStepTypes.backup_now_to_cloud; - } else if (backupProvider === walletBackupTypes.manual) { - stepType = WalletBackupStepTypes.backup_now_manually; - } - - IS_TESTING !== 'true' && + if (!IS_TEST) { Navigation.handleAction(Routes.BACKUP_SHEET, { - step: stepType, + step: WalletBackupStepTypes.backup_prompt, }); + } } }, 1000); @@ -414,6 +409,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) { showImportModal, profilesEnabled, dangerouslyGetParent, + backupProvider, ]); return { diff --git a/src/hooks/useInitializeWallet.ts b/src/hooks/useInitializeWallet.ts index 5f934050e9d..80aa4e903ea 100644 --- a/src/hooks/useInitializeWallet.ts +++ b/src/hooks/useInitializeWallet.ts @@ -68,7 +68,7 @@ export default function useInitializeWallet() { if (shouldRunMigrations && !seedPhrase) { logger.debug('[useInitializeWallet]: shouldRunMigrations && !seedPhrase? => true'); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); logger.debug('[useInitializeWallet]: walletsLoadState call #1'); await runMigrations(); logger.debug('[useInitializeWallet]: done with migrations'); @@ -110,7 +110,7 @@ export default function useInitializeWallet() { if (seedPhrase || isNew) { logger.debug('[useInitializeWallet]: walletsLoadState call #2'); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); } if (isNil(walletAddress)) { diff --git a/src/hooks/useManageCloudBackups.ts b/src/hooks/useManageCloudBackups.ts index 141f26b7f4e..323fd1d62db 100644 --- a/src/hooks/useManageCloudBackups.ts +++ b/src/hooks/useManageCloudBackups.ts @@ -3,12 +3,21 @@ import lang from 'i18n-js'; import { useDispatch } from 'react-redux'; import { cloudPlatform } from '../utils/platform'; import { WrappedAlert as Alert } from '@/helpers/alert'; -import { GoogleDriveUserData, getGoogleAccountUserData, deleteAllBackups, logoutFromGoogleDrive } from '@/handlers/cloudBackup'; -import { clearAllWalletsBackupStatus, updateWalletBackupStatusesBasedOnCloudUserData } from '@/redux/wallets'; +import { + GoogleDriveUserData, + getGoogleAccountUserData, + deleteAllBackups, + logoutFromGoogleDrive as logout, + login, +} from '@/handlers/cloudBackup'; +import { clearAllWalletsBackupStatus } from '@/redux/wallets'; import { showActionSheetWithOptions } from '@/utils'; import { IS_ANDROID } from '@/env'; import { RainbowError, logger } from '@/logger'; import * as i18n from '@/languages'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; +import * as keychain from '@/keychain'; +import { authenticateWithPIN } from '@/handlers/authentication'; export default function useManageCloudBackups() { const dispatch = useDispatch(); @@ -48,10 +57,21 @@ export default function useManageCloudBackups() { await dispatch(clearAllWalletsBackupStatus()); }; + const logoutFromGoogleDrive = async () => { + await logout(); + backupsStore.setState({ + backupProvider: undefined, + backups: { files: [] }, + mostRecentBackup: undefined, + status: CloudBackupState.NotAvailable, + }); + }; + const loginToGoogleDrive = async () => { - await dispatch(updateWalletBackupStatusesBasedOnCloudUserData()); try { + await login(); const accountDetails = await getGoogleAccountUserData(); + backupsStore.getState().syncAndFetchBackups(); setAccountDetails(accountDetails ?? undefined); } catch (error) { logger.error(new RainbowError(`[useManageCloudBackups]: Logging into Google Drive failed.`), { @@ -78,14 +98,36 @@ export default function useManageCloudBackups() { }, async (buttonIndex: any) => { if (buttonIndex === 0) { - if (IS_ANDROID) { - logoutFromGoogleDrive(); - setAccountDetails(undefined); - } - removeBackupStateFromAllWallets(); + try { + let userPIN: string | undefined; + const hasBiometricsEnabled = await keychain.getSupportedBiometryType(); + if (IS_ANDROID && !hasBiometricsEnabled) { + try { + userPIN = (await authenticateWithPIN()) ?? undefined; + } catch (e) { + Alert.alert(i18n.t(i18n.l.back_up.wrong_pin)); + return; + } + } - await deleteAllBackups(); - Alert.alert(lang.t('back_up.backup_deleted_successfully')); + // Prompt for authentication before allowing them to delete backups + await keychain.getAllKeys(); + + if (IS_ANDROID) { + logoutFromGoogleDrive(); + setAccountDetails(undefined); + } + removeBackupStateFromAllWallets(); + + await deleteAllBackups(); + Alert.alert(lang.t('back_up.backup_deleted_successfully')); + } catch (e) { + logger.error(new RainbowError(`[useManageCloudBackups]: Error deleting all backups`), { + error: (e as Error).message, + }); + + Alert.alert(lang.t('back_up.errors.keychain_access')); + } } } ); @@ -94,7 +136,7 @@ export default function useManageCloudBackups() { if (_buttonIndex === 1 && IS_ANDROID) { logoutFromGoogleDrive(); setAccountDetails(undefined); - removeBackupStateFromAllWallets().then(() => loginToGoogleDrive()); + loginToGoogleDrive(); } } ); diff --git a/src/hooks/useUpdateEmoji.ts b/src/hooks/useUpdateEmoji.ts index d38f229ae20..7a6781788b0 100644 --- a/src/hooks/useUpdateEmoji.ts +++ b/src/hooks/useUpdateEmoji.ts @@ -17,11 +17,11 @@ export default function useUpdateEmoji() { const saveInfo = useCallback( async (name: string, color: number) => { const walletId = selectedWallet.id; - const newWallets: typeof wallets = { + const newWallets = { ...wallets, [walletId]: { ...wallets![walletId], - addresses: wallets![walletId].addresses.map((singleAddress: { address: string }) => + addresses: wallets![walletId].addresses.map(singleAddress => singleAddress.address.toLowerCase() === accountAddress.toLowerCase() ? { ...singleAddress, diff --git a/src/hooks/useWalletBalances.ts b/src/hooks/useWalletBalances.ts index fbafc1c909e..9f1e912e437 100644 --- a/src/hooks/useWalletBalances.ts +++ b/src/hooks/useWalletBalances.ts @@ -3,11 +3,7 @@ import { useMemo } from 'react'; import { Address } from 'viem'; import useAccountSettings from './useAccountSettings'; import { useAddysSummary } from '@/resources/summary/summary'; -import { useQueries } from '@tanstack/react-query'; -import { fetchPositions, positionsQueryKey } from '@/resources/defi/PositionsQuery'; -import { RainbowPositions } from '@/resources/defi/types'; import { add, convertAmountToNativeDisplay } from '@/helpers/utilities'; -import { queryClient } from '@/react-query'; const QUERY_CONFIG = { staleTime: 60_000, // 1 minute @@ -43,7 +39,7 @@ const useWalletBalances = (wallets: AllRainbowWallets): WalletBalanceResult => { [wallets] ); - const { data: summaryData, isLoading: isSummaryLoading } = useAddysSummary( + const { data: summaryData, isLoading } = useAddysSummary( { addresses: allAddresses, currency: nativeCurrency, @@ -51,17 +47,6 @@ const useWalletBalances = (wallets: AllRainbowWallets): WalletBalanceResult => { QUERY_CONFIG ); - const positionQueries = useQueries({ - queries: allAddresses.map(address => ({ - queryKey: positionsQueryKey({ address, currency: nativeCurrency }), - queryFn: () => fetchPositions({ address, currency: nativeCurrency }), - enabled: !!address, - ...QUERY_CONFIG, - })), - }); - - const isLoading = isSummaryLoading || positionQueries.some(query => query.isLoading); - const balances = useMemo(() => { const result: Record = {}; @@ -70,9 +55,10 @@ const useWalletBalances = (wallets: AllRainbowWallets): WalletBalanceResult => { for (const address of allAddresses) { const lowerCaseAddress = address.toLowerCase() as Address; const assetBalance = summaryData?.data?.addresses?.[lowerCaseAddress]?.summary?.asset_value?.toString() || '0'; - const positionData = queryClient.getQueryData(positionsQueryKey({ address, currency: nativeCurrency })); - const positionsBalance = positionData ? positionData.totals.total.amount : '0'; - const totalAccountBalance = add(assetBalance, positionsBalance); + const positionsBalance = summaryData?.data?.addresses?.[lowerCaseAddress]?.summary?.positions_value?.toString() || '0'; + const claimablesBalance = summaryData?.data?.addresses?.[lowerCaseAddress]?.summary?.claimables_value?.toString() || '0'; + + const totalAccountBalance = add(assetBalance, add(positionsBalance, claimablesBalance)); result[lowerCaseAddress] = { assetBalanceAmount: assetBalance, diff --git a/src/hooks/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts index 57b9caac681..cb5d6350a5e 100644 --- a/src/hooks/useWalletCloudBackup.ts +++ b/src/hooks/useWalletCloudBackup.ts @@ -1,16 +1,14 @@ -import { captureException } from '@sentry/react-native'; -import lang from 'i18n-js'; import { values } from 'lodash'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { Linking } from 'react-native'; import { useDispatch } from 'react-redux'; -import { addWalletToCloudBackup, backupWalletToCloud, findLatestBackUp } from '../model/backup'; +import { backupWalletToCloud } from '../model/backup'; import { setWalletBackedUp } from '../redux/wallets'; import { cloudPlatform } from '../utils/platform'; import useWallets from './useWallets'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { analytics } from '@/analytics'; -import { CLOUD_BACKUP_ERRORS, isCloudBackupAvailable } from '@/handlers/cloudBackup'; +import { CLOUD_BACKUP_ERRORS, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup'; import WalletBackupTypes from '@/helpers/walletBackupTypes'; import { logger, RainbowError } from '@/logger'; import { getSupportedBiometryType } from '@/keychain'; @@ -41,7 +39,6 @@ export function getUserError(e: Error) { export default function useWalletCloudBackup() { const dispatch = useDispatch(); const { wallets } = useWallets(); - const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]); const walletCloudBackup = useCallback( async ({ @@ -52,36 +49,63 @@ export default function useWalletCloudBackup() { }: { handleNoLatestBackup?: () => void; handlePasswordNotFound?: () => void; - onError?: (error: string) => void; - onSuccess?: () => void; + onError?: (error: string, isDamaged?: boolean) => void; + onSuccess?: (password: string) => void; password: string; walletId: string; }): Promise => { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - analytics.track('iCloud not enabled', { - category: 'backup', - }); - Alert.alert(lang.t('modal.back_up.alerts.cloud_not_enabled.label'), lang.t('modal.back_up.alerts.cloud_not_enabled.description'), [ - { - onPress: () => { - Linking.openURL('https://support.apple.com/en-us/HT204025'); - analytics.track('View how to Enable iCloud', { - category: 'backup', - }); - }, - text: lang.t('modal.back_up.alerts.cloud_not_enabled.show_me'), - }, - { - onPress: () => { - analytics.track('Ignore how to enable iCloud', { - category: 'backup', - }); - }, - style: 'cancel', - text: lang.t('modal.back_up.alerts.cloud_not_enabled.no_thanks'), - }, - ]); + if (IS_ANDROID) { + try { + await login(); + const userData = await getGoogleAccountUserData(); + if (!userData) { + Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); + return false; + } + } catch (e) { + logger.error(new RainbowError('[BackupSheetSectionNoProvider]: No account found'), { + error: e, + }); + Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); + return false; + } + } else { + const isAvailable = await isCloudBackupAvailable(); + if (!isAvailable) { + analytics.track('iCloud not enabled', { + category: 'backup', + }); + Alert.alert( + i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label), + i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description), + [ + { + onPress: () => { + Linking.openURL('https://support.apple.com/en-us/HT204025'); + analytics.track('View how to Enable iCloud', { + category: 'backup', + }); + }, + text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), + }, + { + onPress: () => { + analytics.track('Ignore how to enable iCloud', { + category: 'backup', + }); + }, + style: 'cancel', + text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), + }, + ] + ); + return false; + } + } + + const wallet = wallets?.[walletId]; + if (wallet?.damaged) { + onError?.(i18n.t(i18n.l.back_up.errors.damaged_wallet), true); return false; } @@ -101,23 +125,14 @@ export default function useWalletCloudBackup() { logger.debug('[useWalletCloudBackup]: password fetched correctly'); let updatedBackupFile = null; + try { - if (!latestBackup) { - logger.debug(`[useWalletCloudBackup]: backing up to ${cloudPlatform}: ${wallets![walletId]}`); - updatedBackupFile = await backupWalletToCloud({ - password, - wallet: wallets![walletId], - userPIN, - }); - } else { - logger.debug(`[useWalletCloudBackup]: adding wallet to ${cloudPlatform} backup: ${wallets![walletId]}`); - updatedBackupFile = await addWalletToCloudBackup({ - password, - wallet: wallets![walletId], - filename: latestBackup, - userPIN, - }); - } + logger.debug(`[useWalletCloudBackup]: backing up to ${cloudPlatform}: ${(wallets || {})[walletId]}`); + updatedBackupFile = await backupWalletToCloud({ + password, + wallet: (wallets || {})[walletId], + userPIN, + }); } catch (e: any) { const userError = getUserError(e); !!onError && onError(userError); @@ -134,7 +149,7 @@ export default function useWalletCloudBackup() { logger.debug('[useWalletCloudBackup]: backup completed!'); await dispatch(setWalletBackedUp(walletId, WalletBackupTypes.cloud, updatedBackupFile)); logger.debug('[useWalletCloudBackup]: backup saved everywhere!'); - !!onSuccess && onSuccess(); + !!onSuccess && onSuccess(password); return true; } catch (e) { logger.error(new RainbowError(`[useWalletCloudBackup]: error while trying to save wallet backup state: ${e}`)); @@ -148,7 +163,7 @@ export default function useWalletCloudBackup() { return false; }, - [dispatch, latestBackup, wallets] + [dispatch, wallets] ); return walletCloudBackup; diff --git a/src/hooks/useWallets.ts b/src/hooks/useWallets.ts index 38363886917..20de06f22a1 100644 --- a/src/hooks/useWallets.ts +++ b/src/hooks/useWallets.ts @@ -5,14 +5,12 @@ import { RainbowWallet } from '@/model/wallet'; import { AppState } from '@/redux/store'; const walletSelector = createSelector( - ({ wallets: { isWalletLoading, selected = {} as RainbowWallet, walletNames, wallets } }: AppState) => ({ - isWalletLoading, - selectedWallet: selected as any, + ({ wallets: { selected = {} as RainbowWallet, walletNames, wallets } }: AppState) => ({ + selectedWallet: selected, walletNames, wallets, }), - ({ isWalletLoading, selectedWallet, walletNames, wallets }) => ({ - isWalletLoading, + ({ selectedWallet, walletNames, wallets }) => ({ selectedWallet, walletNames, wallets, @@ -20,13 +18,12 @@ const walletSelector = createSelector( ); export default function useWallets() { - const { isWalletLoading, selectedWallet, walletNames, wallets } = useSelector(walletSelector); + const { selectedWallet, walletNames, wallets } = useSelector(walletSelector); return { isDamaged: selectedWallet?.damaged, isReadOnlyWallet: selectedWallet.type === WalletTypes.readOnly, isHardwareWallet: !!selectedWallet.deviceId, - isWalletLoading, selectedWallet, walletNames, wallets, diff --git a/src/languages/en_US.json b/src/languages/en_US.json index fea3662bb41..c7678568df5 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -96,7 +96,8 @@ "generic": "Error while trying to backup. Error code: %{errorCodes}", "no_keys_found": "No keys found. Please try again.", "backup_not_found": "Backup not found. Please try again.", - "no_account_found": "Unable to retrieve backup files. Make sure you're logged in." + "no_account_found": "Unable to retrieve backup files. Make sure you're logged in.", + "damaged_wallet": "Unable to backup wallet. Missing keychain data." }, "wrong_pin": "The PIN code you entered was incorrect and we can't make a backup. Please try again with the correct code.", "already_backed_up": { @@ -115,6 +116,8 @@ "no_backups": "No backups found", "failed_to_fetch_backups": "Failed to fetch backups", "retry": "Retry", + "refresh": "Refresh", + "syncing_cloud_store": "Syncing to %{cloudPlatformName}", "fetching_backups": "Retrieving backups from %{cloudPlatformName}", "back_up_to_platform": "Back up to %{cloudPlatformName}", "restore_from_platform": "Restore from %{cloudPlatformName}", @@ -137,7 +140,7 @@ "choose_backup_cloud_description": "Securely back up your wallet to %{cloudPlatform} so you can restore it if you lose your device or get a new one.", "choose_backup_manual_description": "Back up your wallet manually by saving your secret phrase in a secure location.", "enable_cloud_backups_description": "If you prefer to back up your wallets manually, you can do so below.", - "latest_backup": "Last Backup: %{date}", + "latest_backup": "Latest Backup: %{date}", "back_up_all_wallets_to_cloud": "Back Up All Wallets to %{cloudPlatformName}", "most_recent_backup": "Most Recent Backup", "out_of_date": "Out of Date", @@ -145,6 +148,12 @@ "older_backups": "Older Backups", "no_older_backups": "No Older Backups", "older_backups_title": "%{date} at %{time}", + "statuses": { + "not_enabled": "Not Enabled", + "syncing": "Syncing", + "out_of_date": "Out of Date", + "up_to_date": "Up to Date" + }, "password": { "a_password_youll_remember_part_one": "This password is", "not": "not", @@ -1220,6 +1229,12 @@ "check_out_this_wallet": "Check out this wallet's collectibles on 🌈 Rainbow at %{showcaseUrl}" } }, + "loading": { + "backing_up": "Backing up...", + "creating_wallet": "Creating wallet...", + "importing_wallet": "Importing...", + "restoring": "Restoring..." + }, "message": { "click_to_copy_to_clipboard": "Click to copy to clipboard", "coming_soon": "Coming soon...", @@ -3026,6 +3041,59 @@ "new_tab": "New Tab" } }, + "trending_tokens": { + "all": "All", + "no_results": { + "title": "No results", + "body": "Try browsing a larger timeframe or a different network or category." + }, + "and": "and", + "and_others": { + "one": "and %{count} other", + "other": "and %{count} others" + }, + "filters": { + "categories": { + "TRENDING": "Trending", + "NEW": "New", + "FARCASTER": "Farcaster" + }, + "sort": { + "RECOMMENDED": { + "label": "Sort", + "menuOption": "Default" + }, + "VOLUME": "Volume", + "MARKET_CAP": "Market Cap", + "TOP_GAINERS": "Top Gainers", + "TOP_LOSERS": "Top Losers" + }, + "time": { + "H12": "12 Hours", + "H12_ABBREVIATED": "12h", + "H24": "24 Hours", + "H24_ABBREVIATED": "24h", + "D7": "1 Week", + "D3": "3 Days" + } + } + }, + "network_switcher": { + "customize_networks_banner": { + "title": "Customize Networks", + "tap_the": "Tap the", + "button_to_set_up": "button below to set up" + }, + "drag_here_to_unpin": "Drop Here to Unpin", + "edit": "Edit", + "networks": "Networks", + "drag_to_rearrange": "Drag to Rearrange", + "show_less": "Show Less", + "more": "More", + "show_more": "More Networks", + "all_networks": "All Networks" + }, + "done": "Done", "copy": "Copy", "paste": "Paste" } diff --git a/src/model/backup.ts b/src/model/backup.ts index 2eb50a7c297..c838796664d 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -1,15 +1,23 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { NativeModules } from 'react-native'; +import { NativeModules, Linking } from 'react-native'; import { captureException } from '@sentry/react-native'; import { endsWith } from 'lodash'; -import { CLOUD_BACKUP_ERRORS, encryptAndSaveDataToCloud, getDataFromCloud } from '@/handlers/cloudBackup'; +import { + CLOUD_BACKUP_ERRORS, + encryptAndSaveDataToCloud, + getDataFromCloud, + isCloudBackupAvailable, + getGoogleAccountUserData, + login, + logoutFromGoogleDrive, + normalizeAndroidBackupFilename, +} from '@/handlers/cloudBackup'; +import { Alert as NativeAlert } from '@/components/alerts'; import WalletBackupTypes from '../helpers/walletBackupTypes'; -import WalletTypes from '../helpers/walletTypes'; -import { Alert } from '@/components/alerts'; import { allWalletsKey, pinKey, privateKeyKey, seedPhraseKey, selectedWalletKey, identifierForVendorKey } from '@/utils/keychainConstants'; import * as keychain from '@/model/keychain'; import * as kc from '@/keychain'; -import { AllRainbowWallets, allWalletsVersion, createWallet, RainbowWallet } from './wallet'; +import { AllRainbowWallets, createWallet, RainbowWallet } from './wallet'; import { analytics } from '@/analytics'; import { logger, RainbowError } from '@/logger'; import { IS_ANDROID, IS_DEV } from '@/env'; @@ -24,16 +32,19 @@ import Routes from '@/navigation/routesNames'; import { clearAllStorages } from './mmkv'; import walletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import { getRemoteConfig } from './remoteConfig'; +import { WrappedAlert as Alert } from '@/helpers/alert'; +import { AppDispatch } from '@/redux/store'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; const { DeviceUUID } = NativeModules; const encryptor = new AesEncryptor(); const PIN_REGEX = /^\d{4}$/; export interface CloudBackups { - files: Backup[]; + files: BackupFile[]; } -export interface Backup { +export interface BackupFile { isDirectory: boolean; isFile: boolean; lastModified: string; @@ -44,8 +55,9 @@ export interface Backup { } export const parseTimestampFromFilename = (filename: string) => { + const name = normalizeAndroidBackupFilename(filename); return Number( - filename + name .replace('.backup_', '') .replace('backup_', '') .replace('.json', '') @@ -54,6 +66,27 @@ export const parseTimestampFromFilename = (filename: string) => { ); }; +/** + * Parse the timestamp from a backup file name + * @param filename - The name of the backup file backup_${now}.json + * @returns The timestamp as a number + */ +export const parseTimestampFromBackupFile = (filename: string | null): number | undefined => { + if (!filename) { + return; + } + const match = filename.match(/backup_(\d+)\.json/); + if (!match) { + return; + } + + if (Number.isNaN(Number(match[1]))) { + return; + } + + return Number(match[1]); +}; + type BackupPassword = string; interface BackedUpData { @@ -63,9 +96,72 @@ interface BackedUpData { export interface BackupUserData { wallets: AllRainbowWallets; } +type MaybePromise = T | Promise; + +export const executeFnIfCloudBackupAvailable = async ({ fn, logout = false }: { fn: () => MaybePromise; logout?: boolean }) => { + backupsStore.getState().setStatus(CloudBackupState.InProgress); + + if (IS_ANDROID) { + try { + if (logout) { + await logoutFromGoogleDrive(); + } + + const currentUser = await getGoogleAccountUserData(); + if (!currentUser) { + await login(); + await backupsStore.getState().syncAndFetchBackups(); + } + + const userData = await getGoogleAccountUserData(); + if (!userData) { + Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); + backupsStore.getState().setStatus(CloudBackupState.NotAvailable); + return; + } + // execute the function + + // NOTE: Set this back to ready in order to process the backup + backupsStore.getState().setStatus(CloudBackupState.Ready); + return await fn(); + } catch (e) { + logger.error(new RainbowError('[BackupSheetSectionNoProvider]: No account found'), { + error: e, + }); + Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); + backupsStore.getState().setStatus(CloudBackupState.NotAvailable); + } + } else { + const isAvailable = await isCloudBackupAvailable(); + if (!isAvailable) { + Alert.alert( + i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label), + i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description), + [ + { + onPress: () => { + Linking.openURL('https://support.apple.com/en-us/HT204025'); + }, + text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), + }, + { + style: 'cancel', + text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), + }, + ] + ); + backupsStore.getState().setStatus(CloudBackupState.NotAvailable); + return; + } + + // NOTE: Set this back to ready in order to process the backup + backupsStore.getState().setStatus(CloudBackupState.Ready); + return await fn(); + } +}; async function extractSecretsForWallet(wallet: RainbowWallet) { - const allKeys = await keychain.loadAllKeys(); + const allKeys = await kc.getAllKeys(); if (!allKeys) throw new Error(CLOUD_BACKUP_ERRORS.KEYCHAIN_ACCESS_ERROR); const secrets = {} as { [key: string]: string }; @@ -100,17 +196,15 @@ async function extractSecretsForWallet(wallet: RainbowWallet) { export async function backupAllWalletsToCloud({ wallets, password, - latestBackup, onError, onSuccess, dispatch, }: { wallets: AllRainbowWallets; password: BackupPassword; - latestBackup: string | null; onError?: (message: string) => void; - onSuccess?: () => void; - dispatch: any; + onSuccess?: (password: BackupPassword) => void; + dispatch: AppDispatch; }) { let userPIN: string | undefined; const hasBiometricsEnabled = await kc.getSupportedBiometryType(); @@ -126,11 +220,9 @@ export async function backupAllWalletsToCloud({ try { /** * Loop over all keys and decrypt if necessary for android - * if no latest backup, create first backup with all secrets - * if latest backup, update updatedAt and add new secrets to the backup */ - const allKeys = await keychain.loadAllKeys(); + const allKeys = await kc.getAllKeys(); if (!allKeys) { onError?.(i18n.t(i18n.l.back_up.errors.no_keys_found)); return; @@ -157,49 +249,21 @@ export async function backupAllWalletsToCloud({ label: cloudPlatform, }); - let updatedBackupFile: any = null; - if (!latestBackup) { - const data = { - createdAt: now, - secrets: {}, - }; - const promises = Object.entries(allSecrets).map(async ([username, password]) => { - const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded({ [username]: password }, userPIN); - - data.secrets = { - ...data.secrets, - ...processedNewSecrets, - }; - }); - - await Promise.all(promises); - updatedBackupFile = await encryptAndSaveDataToCloud(data, password, `backup_${now}.json`); - } else { - // if we have a latest backup file, we need to update the updatedAt and add new secrets to the backup file.. - const backup = await getDataFromCloud(password, latestBackup); - if (!backup) { - onError?.(i18n.t(i18n.l.back_up.errors.backup_not_found)); - return; - } + const data = { + createdAt: now, + secrets: {}, + }; + const promises = Object.entries(allSecrets).map(async ([username, password]) => { + const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded({ [username]: password }, userPIN); - const data = { - createdAt: backup.createdAt, - secrets: backup.secrets, + data.secrets = { + ...data.secrets, + ...processedNewSecrets, }; + }); - const promises = Object.entries(allSecrets).map(async ([username, password]) => { - const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded({ [username]: password }, userPIN); - - data.secrets = { - ...data.secrets, - ...processedNewSecrets, - }; - }); - - await Promise.all(promises); - updatedBackupFile = await encryptAndSaveDataToCloud(data, password, latestBackup); - } - + await Promise.all(promises); + const updatedBackupFile = await encryptAndSaveDataToCloud(data, password, `backup_${now}.json`); const walletIdsToUpdate = Object.keys(wallets); await dispatch(setAllWalletsWithIdsAsBackedUp(walletIdsToUpdate, WalletBackupTypes.cloud, updatedBackupFile)); @@ -209,16 +273,18 @@ export async function backupAllWalletsToCloud({ label: cloudPlatform, }); - onSuccess?.(); - } catch (error: any) { - const userError = getUserError(error); - onError?.(userError); - captureException(error); - analytics.track(`Error backing up all wallets to ${cloudPlatform}`, { - category: 'backup', - error: userError, - label: cloudPlatform, - }); + onSuccess?.(password); + } catch (error) { + if (error instanceof Error) { + const userError = getUserError(error); + onError?.(userError); + captureException(error); + analytics.track(`Error backing up all wallets to ${cloudPlatform}`, { + category: 'backup', + error: userError, + label: cloudPlatform, + }); + } } } @@ -251,9 +317,15 @@ export async function addWalletToCloudBackup({ wallet: RainbowWallet; filename: string; userPIN?: string; -}): Promise { - // @ts-ignore +}): Promise { const backup = await getDataFromCloud(password, filename); + if (!backup) { + logger.error(new RainbowError('[backup]: Unable to get backup data for filename'), { + filename, + }); + return null; + } + const now = Date.now(); const newSecretsToBeAddedToBackup = await extractSecretsForWallet(wallet); const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded(newSecretsToBeAddedToBackup, userPIN); @@ -321,25 +393,6 @@ export async function decryptAllPinEncryptedSecretsIfNeeded(secrets: Record { - // Check if there's a wallet backed up - if (wallet.backedUp && wallet.backupDate && wallet.backupFile && wallet.backupType === WalletBackupTypes.cloud) { - // If there is one, let's grab the latest backup - if (!latestBackup || Number(wallet.backupDate) > latestBackup) { - filename = wallet.backupFile; - latestBackup = Number(wallet.backupDate); - } - } - }); - } - return filename; -} - export const RestoreCloudBackupResultStates = { success: 'success', failedWhenRestoring: 'failedWhenRestoring', @@ -368,16 +421,14 @@ const sanitizeFilename = (filename: string) => { */ export async function restoreCloudBackup({ password, - userData, - nameOfSelectedBackupFile, + backupFilename, }: { password: BackupPassword; - userData: BackupUserData | undefined; - nameOfSelectedBackupFile: string; + backupFilename: string; }): Promise { try { // 1 - sanitize filename to remove extra things we don't care about - const filename = sanitizeFilename(nameOfSelectedBackupFile); + const filename = sanitizeFilename(backupFilename); if (!filename) { return RestoreCloudBackupResultStates.failedWhenRestoring; } @@ -402,26 +453,6 @@ export async function restoreCloudBackup({ } } - if (userData) { - // Restore only wallets that were backed up in cloud - // or wallets that are read-only - const walletsToRestore: AllRainbowWallets = {}; - Object.values(userData?.wallets ?? {}).forEach(wallet => { - if ( - (wallet.backedUp && wallet.backupDate && wallet.backupFile && wallet.backupType === WalletBackupTypes.cloud) || - wallet.type === WalletTypes.readOnly - ) { - walletsToRestore[wallet.id] = wallet; - } - }); - - // All wallets - dataToRestore[allWalletsKey] = { - version: allWalletsVersion, - wallets: walletsToRestore, - }; - } - const restoredSuccessfully = await restoreSpecificBackupIntoKeychain(dataToRestore, userPIN); return restoredSuccessfully ? RestoreCloudBackupResultStates.success : RestoreCloudBackupResultStates.failedWhenRestoring; } catch (error) { @@ -525,74 +556,6 @@ async function restoreSpecificBackupIntoKeychain(backedUpData: BackedUpData, use } } -async function restoreCurrentBackupIntoKeychain(backedUpData: BackedUpData, newPIN?: string): Promise { - try { - // Access control config per each type of key - const privateAccessControlOptions = await keychain.getPrivateAccessControlOptions(); - const encryptedBackupPinData = backedUpData[pinKey]; - const backupPIN = await decryptPIN(encryptedBackupPinData); - - await Promise.all( - Object.keys(backedUpData).map(async key => { - let value = backedUpData[key]; - const theKeyIsASeedPhrase = endsWith(key, seedPhraseKey); - const theKeyIsAPrivateKey = endsWith(key, privateKeyKey); - const accessControl: typeof kc.publicAccessControlOptions = - theKeyIsASeedPhrase || theKeyIsAPrivateKey ? privateAccessControlOptions : kc.publicAccessControlOptions; - - /* - * Backups that were saved encrypted with PIN to the cloud need to be - * decrypted with the backup PIN first, and then if we still need - * to store them as encrypted, - * we need to re-encrypt them with a new PIN - */ - if (theKeyIsASeedPhrase) { - const parsedValue = JSON.parse(value); - parsedValue.seedphrase = await decryptSecretFromBackupPin({ - secret: parsedValue.seedphrase, - backupPIN, - }); - value = JSON.stringify(parsedValue); - } else if (theKeyIsAPrivateKey) { - const parsedValue = JSON.parse(value); - parsedValue.privateKey = await decryptSecretFromBackupPin({ - secret: parsedValue.privateKey, - backupPIN, - }); - value = JSON.stringify(parsedValue); - } - - /* - * Since we're decrypting the data that was saved as PIN code encrypted, - * we will allow the user to create a new PIN code. - * We store the old PIN code in the backup, but we don't want to restore it, - * since it will override the new PIN code that we just saved to keychain. - */ - if (key === pinKey) { - return; - } - - if (typeof value === 'string') { - return kc.set(key, value, { - ...accessControl, - androidEncryptionPin: newPIN, - }); - } else { - return kc.setObject(key, value, { - ...accessControl, - androidEncryptionPin: newPIN, - }); - } - }) - ); - - return true; - } catch (e) { - logger.error(new RainbowError(`[backup]: Error restoring current backup into keychain: ${e}`)); - return false; - } -} - async function decryptSecretFromBackupPin({ secret, backupPIN }: { secret?: string; backupPIN?: string }) { let processedSecret = secret; @@ -638,13 +601,9 @@ export async function saveBackupPassword(password: BackupPassword): Promise { - const rainbowBackupPassword = await keychain.loadString('RainbowBackupPassword'); - if (typeof rainbowBackupPassword === 'number') { - return null; - } - - if (rainbowBackupPassword) { - return rainbowBackupPassword; + const { value } = await kc.get('RainbowBackupPassword'); + if (value) { + return value; } return await fetchBackupPassword(); @@ -653,7 +612,7 @@ export async function getLocalBackupPassword(): Promise { export async function saveLocalBackupPassword(password: string) { const privateAccessControlOptions = await keychain.getPrivateAccessControlOptions(); - await keychain.saveString('RainbowBackupPassword', password, privateAccessControlOptions); + await kc.set('RainbowBackupPassword', password, privateAccessControlOptions); saveBackupPassword(password); } @@ -666,7 +625,7 @@ export async function fetchBackupPassword(): Promise { try { const { value: results } = await kc.getSharedWebCredentials(); if (results) { - return results.password as BackupPassword; + return results.password; } return null; } catch (e) { @@ -695,7 +654,7 @@ export async function getDeviceUUID(): Promise { } const FailureAlert = () => - Alert({ + NativeAlert({ buttons: [ { style: 'cancel', diff --git a/src/model/remoteConfig.ts b/src/model/remoteConfig.ts index 40382af707b..3341a3a0000 100644 --- a/src/model/remoteConfig.ts +++ b/src/model/remoteConfig.ts @@ -57,6 +57,8 @@ export interface RainbowConfig extends Record featured_results: boolean; claimables: boolean; nfts_enabled: boolean; + + trending_tokens_limit: number; } export const DEFAULT_CONFIG: RainbowConfig = { @@ -147,6 +149,8 @@ export const DEFAULT_CONFIG: RainbowConfig = { featured_results: true, claimables: true, nfts_enabled: true, + + trending_tokens_limit: 10, }; export async function fetchRemoteConfig(): Promise { @@ -205,6 +209,8 @@ export async function fetchRemoteConfig(): Promise { key === 'nfts_enabled' ) { config[key] = entry.asBoolean(); + } else if (key === 'trending_tokens_limit') { + config[key] = entry.asNumber(); } else { config[key] = entry.asString(); } diff --git a/src/navigation/HardwareWalletTxNavigator.tsx b/src/navigation/HardwareWalletTxNavigator.tsx index 28e290065dc..4b209dabe30 100644 --- a/src/navigation/HardwareWalletTxNavigator.tsx +++ b/src/navigation/HardwareWalletTxNavigator.tsx @@ -63,7 +63,7 @@ export const HardwareWalletTxNavigator = () => { const { navigate } = useNavigation(); - const deviceId = selectedWallet?.deviceId; + const deviceId = selectedWallet.deviceId ?? ''; const [isReady, setIsReady] = useRecoilState(LedgerIsReadyAtom); const [readyForPolling, setReadyForPolling] = useRecoilState(readyForPollingAtom); const [triggerPollerCleanup, setTriggerPollerCleanup] = useRecoilState(triggerPollerCleanupAtom); diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index d246a004f6a..1e03c2d96a3 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -90,6 +90,9 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimPanel'; import { RootStackParamList } from './types'; +import WalletLoadingListener from '@/components/WalletLoadingListener'; +import { Portal as CMPortal } from '@/react-native-cool-modals/Portal'; +import { NetworkSelector } from '@/components/NetworkSwitcher'; const Stack = createStackNavigator(); const OuterStack = createStackNavigator(); @@ -212,7 +215,7 @@ function BSNavigator() { step === walletBackupStepTypes.restore_from_backup ) { heightForStep = backupSheetSizes.long; - } else if (step === walletBackupStepTypes.no_provider) { + } else if (step === walletBackupStepTypes.backup_prompt) { heightForStep = backupSheetSizes.medium; } @@ -242,6 +245,7 @@ function BSNavigator() { + @@ -272,6 +276,10 @@ const AppContainerWithAnalytics = React.forwardRef + + {/* NOTE: Internally, these use some navigational checks */} + + )); diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx index 8b8755b455b..cf9d0d31b9a 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -70,6 +70,7 @@ import { swapConfig, checkIdentifierSheetConfig, recieveModalSheetConfig, + networkSelectorConfig, } from './config'; import { addCashSheet, emojiPreset, emojiPresetWallet, overlayExpandedPreset, sheetPreset } from './effects'; import { InitialRouteContext } from './initialRoute'; @@ -103,6 +104,9 @@ import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel import { ClaimClaimablePanel } from '@/screens/claimables/ClaimPanel'; import { RootStackParamList } from './types'; import { ChooseWalletGroup } from './ChooseWalletGroup'; +import WalletLoadingListener from '@/components/WalletLoadingListener'; +import { Portal as CMPortal } from '@/react-native-cool-modals/Portal'; +import { NetworkSelector } from '@/components/NetworkSwitcher'; const Stack = createStackNavigator(); const NativeStack = createNativeStackNavigator(); @@ -275,6 +279,7 @@ function NativeStackNavigator() { + @@ -288,6 +293,10 @@ const AppContainerWithAnalytics = React.forwardRef + + {/* NOTE: Internally, these use some navigational checks */} + + )); diff --git a/src/navigation/SwipeNavigator.tsx b/src/navigation/SwipeNavigator.tsx index 33f84504828..781ca8932d4 100644 --- a/src/navigation/SwipeNavigator.tsx +++ b/src/navigation/SwipeNavigator.tsx @@ -15,7 +15,7 @@ import RecyclerListViewScrollToTopProvider, { useRecyclerListViewScrollToTopContext, } from '@/navigation/RecyclerListViewScrollToTopContext'; import DappBrowserScreen from '@/screens/dapp-browser/DappBrowserScreen'; -import { discoverOpenSearchFnRef } from '@/screens/discover/components/DiscoverSearchContainer'; +import { discoverOpenSearchFnRef } from '@/components/Discover/DiscoverSearchContainer'; import { PointsScreen } from '@/screens/points/PointsScreen'; import WalletScreen from '@/screens/WalletScreen'; import { useTheme } from '@/theme'; @@ -39,7 +39,7 @@ import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; import { useBrowserStore } from '@/state/browser/browserStore'; import { opacityWorklet } from '@/__swaps__/utils/swaps'; import ProfileScreen from '../screens/ProfileScreen'; -import DiscoverScreen, { discoverScrollToTopFnRef } from '../screens/discover/DiscoverScreen'; +import DiscoverScreen, { discoverScrollToTopFnRef } from '@/screens/DiscoverScreen'; import { ScrollPositionContext } from './ScrollPositionContext'; import SectionListScrollToTopProvider, { useSectionListScrollToTopContext } from './SectionListScrollToTopContext'; import Routes from './routesNames'; diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx index 5428d27e37b..9097c84e1d6 100644 --- a/src/navigation/config.tsx +++ b/src/navigation/config.tsx @@ -103,12 +103,10 @@ export const getHeightForStep = (step: string) => { case WalletBackupStepTypes.backup_manual: case WalletBackupStepTypes.restore_from_backup: return backupSheetSizes.long; - case WalletBackupStepTypes.no_provider: + case WalletBackupStepTypes.backup_prompt: return backupSheetSizes.medium; case WalletBackupStepTypes.check_identifier: return backupSheetSizes.check_identifier; - case WalletBackupStepTypes.backup_now_manually: - return backupSheetSizes.shorter; default: return backupSheetSizes.short; } @@ -248,6 +246,20 @@ export const consoleSheetConfig = { }), }; +export const networkSelectorConfig = { + options: ({ route: { params = {} } }) => ({ + ...buildCoolModalConfig({ + ...params, + backgroundColor: '#000000B2', + backgroundOpacity: 0.7, + cornerRadius: 0, + springDamping: 1, + topOffset: 0, + transitionDuration: 0.3, + }), + }), +}; + export const panelConfig = { options: ({ route: { params = {} } }) => ({ ...buildCoolModalConfig({ diff --git a/src/navigation/routesNames.ts b/src/navigation/routesNames.ts index 10251e661b0..3615eede1e2 100644 --- a/src/navigation/routesNames.ts +++ b/src/navigation/routesNames.ts @@ -101,6 +101,7 @@ const Routes = { SETTINGS_SECTION_NOTIFICATIONS: 'NotificationsSection', SETTINGS_SECTION_PRIVACY: 'PrivacySection', DAPP_BROWSER_CONTROL_PANEL: 'DappBrowserControlPanel', + NETWORK_SELECTOR: 'NetworkSelector', CLAIM_REWARDS_PANEL: 'ClaimRewardsPanel', } as const; diff --git a/src/navigation/types.ts b/src/navigation/types.ts index cc2c8842ab5..c1877d015da 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -10,6 +10,9 @@ import { Claimable } from '@/resources/addys/claimables/types'; import { WalletconnectApprovalSheetRouteParams, WalletconnectResultType } from '@/walletConnect/types'; import { WalletConnectApprovalSheetType } from '@/helpers/walletConnectApprovalSheetTypes'; import { RainbowPosition } from '@/resources/defi/types'; +import { Address } from 'viem'; +import { SharedValue } from 'react-native-reanimated'; +import { ChainId } from '@/state/backendNetworks/types'; export type PartialNavigatorConfigOptions = Pick['Screen']>[0]>, 'options'>; @@ -31,7 +34,7 @@ export type RootStackParamList = { [Routes.CHANGE_WALLET_SHEET]: { watchOnly: boolean; currentAccountAddress: string; - onChangeWallet: (address: string) => void; + onChangeWallet: (address: Address) => void; }; [Routes.SPEED_UP_AND_CANCEL_BOTTOM_SHEET]: { accentColor?: string; @@ -104,4 +107,9 @@ export type RootStackParamList = { [Routes.POSITION_SHEET]: { position: RainbowPosition; }; + [Routes.NETWORK_SELECTOR]: { + onClose?: VoidFunction; + selected: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; + }; }; diff --git a/src/performance/tracking/index.ts b/src/performance/tracking/index.ts index 425faba867e..81b8fb5e73a 100644 --- a/src/performance/tracking/index.ts +++ b/src/performance/tracking/index.ts @@ -18,7 +18,7 @@ function logDurationIfAppropriate(metric: PerformanceMetricsType, durationInMs: } } -const currentlyTrackedMetrics = new Map(); +export const currentlyTrackedMetrics = new Map(); interface AdditionalParams extends Record { tag?: PerformanceTagsType; diff --git a/src/performance/tracking/types/PerformanceMetrics.ts b/src/performance/tracking/types/PerformanceMetrics.ts index 3baf050eb54..3d272e1e71b 100644 --- a/src/performance/tracking/types/PerformanceMetrics.ts +++ b/src/performance/tracking/types/PerformanceMetrics.ts @@ -11,6 +11,7 @@ export const PerformanceMetrics = { initializeWalletconnect: 'Performance WalletConnect Initialize Time', quoteFetching: 'Performance Quote Fetching Time', + timeSpentOnDiscoverScreen: 'Time spent on the Discover screen', } as const; export type PerformanceMetricsType = (typeof PerformanceMetrics)[keyof typeof PerformanceMetrics]; diff --git a/src/react-native-cool-modals/Portal.js b/src/react-native-cool-modals/Portal.js deleted file mode 100644 index 5d03cdadeb8..00000000000 --- a/src/react-native-cool-modals/Portal.js +++ /dev/null @@ -1,50 +0,0 @@ -import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { Platform, requireNativeComponent, StyleSheet, View } from 'react-native'; - -const NativePortalContext = createContext(); - -export function usePortal() { - return useContext(NativePortalContext); -} - -const NativePortal = Platform.OS === 'ios' ? requireNativeComponent('WindowPortal') : View; - -const Wrapper = Platform.OS === 'ios' ? ({ children }) => children : View; - -export function Portal({ children }) { - const [Component, setComponentState] = useState(null); - const [blockTouches, setBlockTouches] = useState(false); - - const hide = useCallback(() => { - setComponentState(); - setBlockTouches(false); - }, []); - - const setComponent = useCallback((value, blockTouches) => { - setComponentState(value); - setBlockTouches(blockTouches); - }, []); - - const contextValue = useMemo( - () => ({ - hide, - setComponent, - }), - [hide, setComponent] - ); - - return ( - - - {children} - - {Component} - - - - ); -} diff --git a/src/react-native-cool-modals/Portal.tsx b/src/react-native-cool-modals/Portal.tsx new file mode 100644 index 00000000000..dd2830ee0b4 --- /dev/null +++ b/src/react-native-cool-modals/Portal.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { IS_IOS } from '@/env'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; +import { requireNativeComponent, StyleSheet, View } from 'react-native'; +import Routes from '@/navigation/routesNames'; +import { useActiveRoute } from '@/hooks/useActiveRoute'; + +const NativePortal = IS_IOS ? requireNativeComponent('WindowPortal') : View; +const Wrapper = IS_IOS ? ({ children }: { children: React.ReactNode }) => children : View; + +export function Portal() { + const activeRoute = useActiveRoute(); + + const { blockTouches, Component } = walletLoadingStore(state => ({ + blockTouches: state.blockTouches, + Component: state.Component, + })); + + if (!Component || (activeRoute === Routes.PIN_AUTHENTICATION_SCREEN && !IS_IOS)) { + return null; + } + + console.log('blockTouches', blockTouches); + + return ( + + + {Component} + + + ); +} diff --git a/src/redux/settings.ts b/src/redux/settings.ts index ce19a5f6131..4535437ea7c 100644 --- a/src/redux/settings.ts +++ b/src/redux/settings.ts @@ -24,6 +24,7 @@ import { getProvider } from '@/handlers/web3'; import { AppState } from '@/redux/store'; import { logger, RainbowError } from '@/logger'; import { Network, ChainId } from '@/state/backendNetworks/types'; +import { Address } from 'viem'; // -- Constants ------------------------------------------------------------- // const SETTINGS_UPDATE_SETTINGS_ADDRESS = 'settings/SETTINGS_UPDATE_SETTINGS_ADDRESS'; @@ -41,7 +42,7 @@ const SETTINGS_UPDATE_ACCOUNT_SETTINGS_SUCCESS = 'settings/SETTINGS_UPDATE_ACCOU */ interface SettingsState { appIcon: string; - accountAddress: string; + accountAddress: Address; chainId: number; language: Language; nativeCurrency: NativeCurrencyKey; @@ -205,7 +206,7 @@ export const settingsChangeAppIcon = (appIcon: string) => (dispatch: Dispatch async (dispatch: Dispatch) => { dispatch({ - payload: accountAddress, + payload: accountAddress as Address, type: SETTINGS_UPDATE_SETTINGS_ADDRESS, }); }; @@ -254,7 +255,7 @@ export const settingsChangeNativeCurrency = // -- Reducer --------------------------------------------------------------- // export const INITIAL_STATE: SettingsState = { - accountAddress: '', + accountAddress: '' as Address, appIcon: 'og', chainId: 1, language: Language.EN_US, diff --git a/src/redux/wallets.ts b/src/redux/wallets.ts index deb49a5ea9b..d17f8b4c0d8 100644 --- a/src/redux/wallets.ts +++ b/src/redux/wallets.ts @@ -3,10 +3,8 @@ import { toChecksumAddress } from 'ethereumjs-util'; import { isEmpty, keys } from 'lodash'; import { Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; -import { backupUserDataIntoCloud, fetchUserDataFromCloud } from '../handlers/cloudBackup'; import { saveKeychainIntegrityState } from '../handlers/localstorage/globalSettings'; import { getWalletNames, saveWalletNames } from '../handlers/localstorage/walletNames'; -import WalletBackupTypes from '../helpers/walletBackupTypes'; import WalletTypes from '../helpers/walletTypes'; import { fetchENSAvatar } from '../hooks/useENSAvatar'; import { hasKey } from '../model/keychain'; @@ -30,6 +28,7 @@ import { AppGetState, AppState } from './store'; import { fetchReverseRecord } from '@/handlers/ens'; import { lightModeThemeColors } from '@/styles'; import { RainbowError, logger } from '@/logger'; +import { parseTimestampFromBackupFile } from '@/model/backup'; // -- Types ---------------------------------------- // @@ -37,11 +36,6 @@ import { RainbowError, logger } from '@/logger'; * The current state of the `wallets` reducer. */ interface WalletsState { - /** - * The current loading state of the wallet. - */ - isWalletLoading: any; - /** * The currently selected wallet. */ @@ -62,21 +56,12 @@ interface WalletsState { * An action for the `wallets` reducer. */ type WalletsAction = - | WalletsSetIsLoadingAction | WalletsSetSelectedAction | WalletsUpdateAction | WalletsUpdateNamesAction | WalletsLoadAction | WalletsAddedAccountAction; -/** - * An action that sets the wallet loading state. - */ -interface WalletsSetIsLoadingAction { - type: typeof WALLETS_SET_IS_LOADING; - payload: WalletsState['isWalletLoading']; -} - /** * An action that sets the selected wallet. */ @@ -130,90 +115,88 @@ const WALLETS_SET_SELECTED = 'wallets/SET_SELECTED'; /** * Loads wallet information from storage and updates state accordingly. */ -export const walletsLoadState = - (profilesEnabled = false) => - async (dispatch: ThunkDispatch, getState: AppGetState) => { - try { - const { accountAddress } = getState().settings; - let addressFromKeychain: string | null = accountAddress; - const allWalletsResult = await getAllWallets(); - const wallets = allWalletsResult?.wallets || {}; - if (isEmpty(wallets)) return; - const selected = await getSelectedWallet(); - // Prevent irrecoverable state (no selected wallet) - let selectedWallet = selected?.wallet; - // Check if the selected wallet is among all the wallets - if (selectedWallet && !wallets[selectedWallet.id]) { - // If not then we should clear it and default to the first one - const firstWalletKey = Object.keys(wallets)[0]; - selectedWallet = wallets[firstWalletKey]; - await setSelectedWallet(selectedWallet); - } +export const walletsLoadState = () => async (dispatch: ThunkDispatch, getState: AppGetState) => { + try { + const { accountAddress } = getState().settings; + let addressFromKeychain: string | null = accountAddress; + const allWalletsResult = await getAllWallets(); + const wallets = allWalletsResult?.wallets || {}; + if (isEmpty(wallets)) return; + const selected = await getSelectedWallet(); + // Prevent irrecoverable state (no selected wallet) + let selectedWallet = selected?.wallet; + // Check if the selected wallet is among all the wallets + if (selectedWallet && !wallets[selectedWallet.id]) { + // If not then we should clear it and default to the first one + const firstWalletKey = Object.keys(wallets)[0]; + selectedWallet = wallets[firstWalletKey]; + await setSelectedWallet(selectedWallet); + } - if (!selectedWallet) { - const address = await loadAddress(); - if (!address) { - selectedWallet = wallets[Object.keys(wallets)[0]]; - } else { - keys(wallets).some(key => { - const someWallet = wallets[key]; - const found = (someWallet.addresses || []).some(account => { - return toChecksumAddress(account.address) === toChecksumAddress(address!); - }); - if (found) { - selectedWallet = someWallet; - logger.debug('[redux/wallets]: Found selected wallet based on loadAddress result'); - } - return found; + if (!selectedWallet) { + const address = await loadAddress(); + if (!address) { + selectedWallet = wallets[Object.keys(wallets)[0]]; + } else { + keys(wallets).some(key => { + const someWallet = wallets[key]; + const found = (someWallet.addresses || []).some(account => { + return toChecksumAddress(account.address) === toChecksumAddress(address!); }); - } + if (found) { + selectedWallet = someWallet; + logger.debug('[redux/wallets]: Found selected wallet based on loadAddress result'); + } + return found; + }); } + } - // Recover from broken state (account address not in selected wallet) - if (!addressFromKeychain) { - addressFromKeychain = await loadAddress(); - logger.debug("[redux/wallets]: addressFromKeychain wasn't set on settings so it is being loaded from loadAddress"); - } + // Recover from broken state (account address not in selected wallet) + if (!addressFromKeychain) { + addressFromKeychain = await loadAddress(); + logger.debug("[redux/wallets]: addressFromKeychain wasn't set on settings so it is being loaded from loadAddress"); + } - const selectedAddress = selectedWallet?.addresses.find(a => { - return a.visible && a.address === addressFromKeychain; - }); + const selectedAddress = selectedWallet?.addresses.find(a => { + return a.visible && a.address === addressFromKeychain; + }); - // Let's select the first visible account if we don't have a selected address - if (!selectedAddress) { - const allWallets = Object.values(allWalletsResult?.wallets || {}); - let account = null; - for (const wallet of allWallets) { - for (const rainbowAccount of wallet.addresses || []) { - if (rainbowAccount.visible) { - account = rainbowAccount; - break; - } + // Let's select the first visible account if we don't have a selected address + if (!selectedAddress) { + const allWallets = Object.values(allWalletsResult?.wallets || {}); + let account = null; + for (const wallet of allWallets) { + for (const rainbowAccount of wallet.addresses || []) { + if (rainbowAccount.visible) { + account = rainbowAccount; + break; } } - if (!account) return; - await dispatch(settingsUpdateAccountAddress(account.address)); - await saveAddress(account.address); - logger.debug('[redux/wallets]: Selected the first visible address because there was not selected one'); } + if (!account) return; + await dispatch(settingsUpdateAccountAddress(account.address)); + await saveAddress(account.address); + logger.debug('[redux/wallets]: Selected the first visible address because there was not selected one'); + } - const walletNames = await getWalletNames(); - dispatch({ - payload: { - selected: selectedWallet, - walletNames, - wallets, - }, - type: WALLETS_LOAD, - }); + const walletNames = await getWalletNames(); + dispatch({ + payload: { + selected: selectedWallet, + walletNames, + wallets, + }, + type: WALLETS_LOAD, + }); - return wallets; - } catch (error) { - logger.error(new RainbowError('[redux/wallets]: Exception during walletsLoadState'), { - message: (error as Error)?.message, - }); - } - }; + return wallets; + } catch (error) { + logger.error(new RainbowError('[redux/wallets]: Exception during walletsLoadState'), { + message: (error as Error)?.message, + }); + } +}; /** * Saves new wallets to storage and updates state accordingly. @@ -252,21 +235,21 @@ export const walletsSetSelected = (wallet: RainbowWallet) => async (dispatch: Di * @param updateUserMetadata Whether to update user metadata. */ export const setAllWalletsWithIdsAsBackedUp = - ( - walletIds: RainbowWallet['id'][], - method: RainbowWallet['backupType'], - backupFile: RainbowWallet['backupFile'] = null, - updateUserMetadata = true - ) => + (walletIds: RainbowWallet['id'][], method: RainbowWallet['backupType'], backupFile: RainbowWallet['backupFile'] = null) => async (dispatch: ThunkDispatch, getState: AppGetState) => { const { wallets, selected } = getState().wallets; const newWallets = { ...wallets }; + let backupDate = Date.now(); + if (backupFile) { + backupDate = parseTimestampFromBackupFile(backupFile) ?? Date.now(); + } + walletIds.forEach(walletId => { newWallets[walletId] = { ...newWallets[walletId], backedUp: true, - backupDate: Date.now(), + backupDate, backupFile, backupType: method, }; @@ -276,17 +259,6 @@ export const setAllWalletsWithIdsAsBackedUp = if (selected?.id && walletIds.includes(selected?.id)) { await dispatch(walletsSetSelected(newWallets[selected.id])); } - - if (method === WalletBackupTypes.cloud && updateUserMetadata) { - try { - await backupUserDataIntoCloud({ wallets: newWallets }); - } catch (e) { - logger.error(new RainbowError('[redux/wallets]: Saving multiple wallets UserData to cloud failed.'), { - message: (e as Error)?.message, - }); - throw e; - } - } }; /** @@ -296,122 +268,28 @@ export const setAllWalletsWithIdsAsBackedUp = * @param walletId The ID of the wallet to modify. * @param method The backup type used. * @param backupFile The backup file, if present. - * @param updateUserMetadata Whether to update user metadata. */ export const setWalletBackedUp = - ( - walletId: RainbowWallet['id'], - method: RainbowWallet['backupType'], - backupFile: RainbowWallet['backupFile'] = null, - updateUserMetadata = true - ) => + (walletId: RainbowWallet['id'], method: RainbowWallet['backupType'], backupFile: RainbowWallet['backupFile'] = null) => async (dispatch: ThunkDispatch, getState: AppGetState) => { const { wallets, selected } = getState().wallets; const newWallets = { ...wallets }; + let backupDate = Date.now(); + if (backupFile) { + backupDate = parseTimestampFromBackupFile(backupFile) ?? Date.now(); + } newWallets[walletId] = { ...newWallets[walletId], backedUp: true, - backupDate: Date.now(), + backupDate, backupFile, backupType: method, }; await dispatch(walletsUpdate(newWallets)); - if (selected!.id === walletId) { + if (selected?.id === walletId) { await dispatch(walletsSetSelected(newWallets[walletId])); } - - if (method === WalletBackupTypes.cloud && updateUserMetadata) { - try { - await backupUserDataIntoCloud({ wallets: newWallets }); - } catch (e) { - logger.error(new RainbowError('[redux/wallets]: Saving wallet UserData to cloud failed.'), { - message: (e as Error)?.message, - }); - throw e; - } - } - }; - -/** - * Grabs user data stored in the cloud and based on this data marks wallets - * as backed up or not - */ -export const updateWalletBackupStatusesBasedOnCloudUserData = - () => async (dispatch: ThunkDispatch, getState: AppGetState) => { - const { wallets, selected } = getState().wallets; - const newWallets = { ...wallets }; - - let currentUserData: { wallets: { [p: string]: RainbowWallet } } | undefined; - try { - currentUserData = await fetchUserDataFromCloud(); - } catch (error) { - logger.error(new RainbowError('[redux/wallets]: There was an error when trying to update wallet backup statuses'), { - error: (error as Error).message, - }); - return; - } - if (currentUserData === undefined) { - return; - } - - // build hashmap of address to wallet based on backup metadata - const addressToWalletLookup = new Map(); - Object.values(currentUserData.wallets).forEach(wallet => { - wallet.addresses?.forEach(account => { - addressToWalletLookup.set(account.address, wallet); - }); - }); - - /* - marking wallet as already backed up if all addresses are backed up properly - and linked to the same wallet - - we assume it's not backed up if: - * we don't have an address in the backup metadata - * we have an address in the backup metadata, but it's linked to multiple - wallet ids (should never happen, but that's a sanity check) - */ - Object.values(newWallets).forEach(wallet => { - const localWalletId = wallet.id; - - let relatedCloudWalletId: string | null = null; - for (const account of wallet.addresses || []) { - const walletDataForCurrentAddress = addressToWalletLookup.get(account.address); - if (!walletDataForCurrentAddress) { - return; - } - if (relatedCloudWalletId === null) { - relatedCloudWalletId = walletDataForCurrentAddress.id; - } else if (relatedCloudWalletId !== walletDataForCurrentAddress.id) { - logger.warn( - '[redux/wallets]: Wallet address is linked to multiple or different accounts in the cloud backup metadata. It could mean that there is an issue with the cloud backup metadata.' - ); - return; - } - } - - if (relatedCloudWalletId === null) { - return; - } - - // update only if we checked the wallet is actually backed up - const cloudBackupData = currentUserData?.wallets[relatedCloudWalletId]; - if (cloudBackupData) { - newWallets[localWalletId] = { - ...newWallets[localWalletId], - backedUp: cloudBackupData.backedUp, - backupDate: cloudBackupData.backupDate, - backupFile: cloudBackupData.backupFile, - backupType: cloudBackupData.backupType, - }; - } - }); - - await dispatch(walletsUpdate(newWallets)); - if (selected?.id) { - await dispatch(walletsSetSelected(newWallets[selected.id])); - } }; /** @@ -706,7 +584,6 @@ export const checkKeychainIntegrity = () => async (dispatch: ThunkDispatch { switch (action.type) { - case WALLETS_SET_IS_LOADING: - return { ...state, isWalletLoading: action.payload }; case WALLETS_SET_SELECTED: return { ...state, selected: action.payload }; case WALLETS_UPDATE: diff --git a/src/resources/metadata/sharedQueries.js b/src/resources/metadata/sharedQueries.js index 5937062f69b..4c348349eb4 100644 --- a/src/resources/metadata/sharedQueries.js +++ b/src/resources/metadata/sharedQueries.js @@ -4,6 +4,10 @@ const BACKEND_NETWORKS_QUERY = ` id name label + colors { + light + dark + } icons { badgeURL } diff --git a/src/resources/summary/summary.ts b/src/resources/summary/summary.ts index cd0ef1d8542..71c68ff92b5 100644 --- a/src/resources/summary/summary.ts +++ b/src/resources/summary/summary.ts @@ -51,6 +51,8 @@ interface AddysSummary { num_erc20s: number; last_activity: number; asset_value: number | null; + claimables_value: number | null; + positions_value: number | null; }; }; summary_by_chain: { @@ -63,6 +65,8 @@ interface AddysSummary { num_erc20s: number; last_activity: number; asset_value: number | null; + claimables_value: number | null; + positions_value: number | null; }; }; }; diff --git a/src/resources/transactions/consolidatedTransactions.ts b/src/resources/transactions/consolidatedTransactions.ts index a749e2f72d0..26db739a248 100644 --- a/src/resources/transactions/consolidatedTransactions.ts +++ b/src/resources/transactions/consolidatedTransactions.ts @@ -135,6 +135,7 @@ export function useConsolidatedTransactions( keepPreviousData: true, getNextPageParam: lastPage => lastPage?.nextPage, refetchInterval: CONSOLIDATED_TRANSACTIONS_INTERVAL, + enabled: !!address, retry: 3, } ); diff --git a/src/resources/trendingTokens/trendingTokens.ts b/src/resources/trendingTokens/trendingTokens.ts index 169a24ab6f4..4bbb62e9308 100644 --- a/src/resources/trendingTokens/trendingTokens.ts +++ b/src/resources/trendingTokens/trendingTokens.ts @@ -2,23 +2,117 @@ import { QueryConfigWithSelect, createQueryKey } from '@/react-query'; import { useQuery } from '@tanstack/react-query'; import { arcClient } from '@/graphql'; -export type TrendingTokensVariables = Parameters['0']; -export type TrendingTokens = Awaited>; +import { TrendingCategory, TrendingSort, TrendingTimeframe } from '@/state/trendingTokens/trendingTokens'; +import { Address } from 'viem'; +import { NativeCurrencyKey } from '@/entities'; +import store from '@/redux/store'; +import { SortDirection } from '@/graphql/__generated__/arc'; +import { UniqueId } from '@/__swaps__/types/assets'; +import { ChainId } from '@/state/backendNetworks/types'; + +export type FarcasterUser = { + username: string; + pfp_url: string; +}; +export type TrendingToken = { + uniqueId: UniqueId; + chainId: ChainId; + address: string; + name: string; + symbol: string; + decimals: number; + price: number; + priceChange: { + hr: number; + day: number; + }; + marketCap: number; + volume: number; + highlightedFriends: FarcasterUser[]; + colors: { + primary: string; + }; + icon_url: string; +}; // /////////////////////////////////////////////// // Query Key -export const trendingTokensQueryKey = (props: TrendingTokensVariables) => createQueryKey('trending-tokens', props, { persisterVersion: 0 }); +export const trendingTokensQueryKey = (props: FetchTrendingTokensArgs) => createQueryKey('trending-tokens', props, { persisterVersion: 2 }); export type TrendingTokensQueryKey = ReturnType; +type FetchTrendingTokensArgs = { + chainId?: ChainId; + category: TrendingCategory; + sortBy: TrendingSort; + sortDirection: SortDirection | undefined; + timeframe: TrendingTimeframe; + walletAddress: Address | undefined; + limit?: number; + currency?: NativeCurrencyKey; +}; + +async function fetchTrendingTokens({ + queryKey: [ + { currency = store.getState().settings.nativeCurrency, category, sortBy, sortDirection, timeframe, walletAddress, chainId, limit }, + ], +}: { + queryKey: TrendingTokensQueryKey; +}) { + const response = await arcClient.trendingTokens({ + category, + sortBy, + sortDirection, + timeframe, + walletAddress, + limit, + chainId, + currency: currency.toLowerCase(), + }); + const trendingTokens: TrendingToken[] = []; + + for (const token of response.trendingTokens.data) { + const { uniqueId, address, name, symbol, chainId, decimals, trending, market, icon_url, colors } = token; + const { bought_stats } = trending.swap_data; + const highlightedFriends = (bought_stats.farcaster_users || []).reduce((friends, friend) => { + const { username, pfp_url } = friend; + if (username && pfp_url) friends.push({ username, pfp_url }); + return friends; + }, [] as FarcasterUser[]); + + trendingTokens.push({ + uniqueId, + chainId: chainId as ChainId, + address, + name, + symbol, + decimals, + price: market.price?.value || 0, + priceChange: { + hr: trending.pool_data.h1_price_change || 0, + day: trending.pool_data.h24_price_change || 0, + }, + marketCap: market.market_cap?.value || 0, + volume: market.volume_24h || 0, + highlightedFriends, + icon_url, + colors: { + primary: colors.primary, + }, + }); + } + + return trendingTokens; +} + // /////////////////////////////////////////////// // Query Hook -export function useTrendingTokens( - props: TrendingTokensVariables, - config: QueryConfigWithSelect = {} +export function useTrendingTokens( + args: FetchTrendingTokensArgs, + config: QueryConfigWithSelect = {} ) { - return useQuery(trendingTokensQueryKey(props), () => arcClient.trendingTokens(props), { + return useQuery(trendingTokensQueryKey(args), fetchTrendingTokens, { ...config, staleTime: 60_000, // 1 minute cacheTime: 60_000 * 30, // 30 minutes diff --git a/src/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx index 7aeee3fc6c4..b0d4450cb22 100644 --- a/src/screens/AddWalletSheet.tsx +++ b/src/screens/AddWalletSheet.tsx @@ -7,24 +7,16 @@ import React from 'react'; import * as i18n from '@/languages'; import { HARDWARE_WALLETS, useExperimentalFlag } from '@/config'; import { analytics, analyticsV2 } from '@/analytics'; -import { InteractionManager, Linking } from 'react-native'; +import { InteractionManager } from 'react-native'; import { logger, RainbowError } from '@/logger'; import WalletsAndBackup from '@/assets/WalletsAndBackup.png'; import CreateNewWallet from '@/assets/CreateNewWallet.png'; import PairHairwareWallet from '@/assets/PairHardwareWallet.png'; import ImportSecretPhraseOrPrivateKey from '@/assets/ImportSecretPhraseOrPrivateKey.png'; import WatchWalletIcon from '@/assets/watchWallet.png'; -import { - getGoogleAccountUserData, - GoogleDriveUserData, - isCloudBackupAvailable, - login, - logoutFromGoogleDrive, -} from '@/handlers/cloudBackup'; import { cloudPlatform } from '@/utils/platform'; -import { IS_ANDROID } from '@/env'; import { RouteProp, useRoute } from '@react-navigation/native'; -import { WrappedAlert as Alert } from '@/helpers/alert'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; const TRANSLATIONS = i18n.l.wallet.new.add_wallet_sheet; @@ -90,47 +82,11 @@ export const AddWalletSheet = () => { isFirstWallet, type: 'seed', }); - if (IS_ANDROID) { - try { - await logoutFromGoogleDrive(); - await login(); - - getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { - if (accountDetails) { - return navigate(Routes.RESTORE_SHEET); - } - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - }); - } catch (e) { - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - logger.error(new RainbowError('[AddWalletSheet]: Error while trying to restore from cloud'), { - error: e, - }); - } - } else { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - Alert.alert( - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label), - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description), - [ - { - onPress: () => { - Linking.openURL('https://support.apple.com/en-us/HT204025'); - }, - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), - }, - { - style: 'cancel', - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), - }, - ] - ); - return; - } - - navigate(Routes.RESTORE_SHEET); - } + + executeFnIfCloudBackupAvailable({ + fn: () => navigate(Routes.RESTORE_SHEET), + logout: true, + }); }; const restoreFromCloudDescription = i18n.t(TRANSLATIONS.options.cloud.description_restore_sheet, { diff --git a/src/screens/discover/DiscoverScreen.tsx b/src/screens/DiscoverScreen.tsx similarity index 95% rename from src/screens/discover/DiscoverScreen.tsx rename to src/screens/DiscoverScreen.tsx index 601066d6260..2a0d43df6a2 100644 --- a/src/screens/discover/DiscoverScreen.tsx +++ b/src/screens/DiscoverScreen.tsx @@ -4,7 +4,7 @@ import { useIsFocused } from '@react-navigation/native'; import { Box } from '@/design-system'; import { Page } from '@/components/layout'; import { Navbar } from '@/components/navbar/Navbar'; -import DiscoverScreenContent from './components/DiscoverScreenContent'; +import DiscoverScreenContent from '@/components/Discover/DiscoverScreenContent'; import { ButtonPressAnimation } from '@/components/animations'; import { ContactAvatar } from '@/components/contacts'; import ImageAvatar from '@/components/contacts/ImageAvatar'; @@ -14,7 +14,7 @@ import { useNavigation } from '@/navigation'; import { safeAreaInsetValues } from '@/utils'; import * as i18n from '@/languages'; import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'; -import DiscoverScreenProvider, { useDiscoverScreenContext } from './DiscoverScreenContext'; +import DiscoverScreenProvider, { useDiscoverScreenContext } from '@/components/Discover/DiscoverScreenContext'; export let discoverScrollToTopFnRef: () => number | null = () => null; @@ -30,18 +30,18 @@ const Content = () => { navigate(Routes.CHANGE_WALLET_SHEET); }, [navigate]); - React.useEffect(() => { - if (isSearching && !isFocused) { - Keyboard.dismiss(); - } - }, [isFocused, isSearching]); - const scrollHandler = useAnimatedScrollHandler({ onScroll: event => { scrollY.value = event.contentOffset.y; }, }); + useEffect(() => { + if (isSearching && !isFocused) { + Keyboard.dismiss(); + } + }, [isFocused, isSearching]); + useEffect(() => { discoverScrollToTopFnRef = scrollToTop; }, [scrollToTop]); diff --git a/src/screens/ExplainSheet.js b/src/screens/ExplainSheet.js index d8945923c99..9e30b06daa6 100644 --- a/src/screens/ExplainSheet.js +++ b/src/screens/ExplainSheet.js @@ -172,6 +172,9 @@ export const explainers = (params, theme) => { const chainId = params?.chainId; const fromChainId = params?.fromChainId; const toChainId = params?.toChainId; + const isDarkMode = theme?.isDarkMode; + + const color = useBackendNetworksStore.getState().getColorsForChainId(chainId, isDarkMode); const chainsLabel = useBackendNetworksStore.getState().getChainsLabel(); @@ -432,8 +435,8 @@ export const explainers = (params, theme) => { swapResetInputs: { button: { label: `Continue with ${chainsLabel[chainId]}`, - bgColor: colors?.networkColors[chainId] && colors?.alpha(colors?.networkColors[chainId], 0.06), - textColor: colors?.networkColors?.[chainId], + bgColor: color && colors?.alpha(color, 0.06), + textColor: color, }, emoji: '🔐', extraHeight: -90, diff --git a/src/screens/RestoreSheet.tsx b/src/screens/RestoreSheet.tsx index 4a3e324bb65..f8186c86341 100644 --- a/src/screens/RestoreSheet.tsx +++ b/src/screens/RestoreSheet.tsx @@ -1,5 +1,5 @@ import { RouteProp, useRoute } from '@react-navigation/native'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import RestoreCloudStep from '../components/backup/RestoreCloudStep'; import ChooseBackupStep from '@/components/backup/ChooseBackupStep'; import Routes from '@/navigation/routesNames'; diff --git a/src/screens/SendSheet.tsx b/src/screens/SendSheet.tsx index f7f71b7173f..c634befe7b0 100644 --- a/src/screens/SendSheet.tsx +++ b/src/screens/SendSheet.tsx @@ -50,7 +50,7 @@ import Routes from '@/navigation/routesNames'; import styled from '@/styled-thing'; import { borders } from '@/styles'; import { convertAmountAndPriceToNativeDisplay, convertAmountFromNativeValue, formatInputDecimals, lessThan } from '@/helpers/utilities'; -import { deviceUtils, ethereumUtils, getUniqueTokenType, safeAreaInsetValues } from '@/utils'; +import { deviceUtils, ethereumUtils, getUniqueTokenType, isLowerCaseMatch, safeAreaInsetValues } from '@/utils'; import { logger, RainbowError } from '@/logger'; import { IS_ANDROID, IS_IOS } from '@/env'; import { NoResults } from '@/components/list'; @@ -62,13 +62,14 @@ import { getNextNonce } from '@/state/nonces'; import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { REGISTRATION_STEPS } from '@/helpers/ens'; -import { useUserAssetsStore } from '@/state/assets/userAssets'; import { ChainId } from '@/state/backendNetworks/types'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { RootStackParamList } from '@/navigation/types'; import { ThemeContextProps, useTheme } from '@/theme'; import { StaticJsonRpcProvider } from '@ethersproject/providers'; import { Contact } from '@/redux/contacts'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; +import store from '@/redux/store'; const sheetHeight = deviceUtils.dimensions.height - (IS_ANDROID ? 30 : 10); const statusBarHeight = IS_IOS ? safeAreaInsetValues.top : StatusBar.currentHeight; @@ -95,6 +96,17 @@ const SheetContainer = styled(Column).attrs({ }); const validateRecipient = (toAddress?: string, tokenAddress?: string) => { + const { wallets } = store.getState().wallets; + // check for if the recipient is in a damaged wallet state and prevent + if (wallets) { + const internalWallet = Object.values(wallets).find(wallet => + wallet.addresses.some(address => isLowerCaseMatch(address.address, toAddress)) + ); + if (internalWallet?.damaged) { + return false; + } + } + if (!toAddress || toAddress?.toLowerCase() === tokenAddress?.toLowerCase()) { return false; } diff --git a/src/screens/SettingsSheet/SettingsSheet.tsx b/src/screens/SettingsSheet/SettingsSheet.tsx index 7a68ad83d86..094cdc17456 100644 --- a/src/screens/SettingsSheet/SettingsSheet.tsx +++ b/src/screens/SettingsSheet/SettingsSheet.tsx @@ -21,7 +21,6 @@ import { useDimensions } from '@/hooks'; import { SETTINGS_BACKUP_ROUTES } from './components/Backups/routes'; import { IS_ANDROID } from '@/env'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { CloudBackupProvider } from '@/components/backup/CloudBackupProvider'; const Stack = createStackNavigator(); @@ -52,102 +51,100 @@ export function SettingsSheet() { const memoSettingsOptions = useMemo(() => settingsOptions(colors), [colors]); return ( - - - {({ backgroundColor }) => ( - + {({ backgroundColor }) => ( + + - - - {() => ( - - )} - - {Object.values(SettingsPages).map( - ({ component, getTitle, key }) => - component && ( - - ) + {() => ( + )} - ({ - cardStyleInterpolator: settingsCardStyleInterpolator, - title: route.params?.title, - })} - /> - ({ - cardStyleInterpolator: settingsCardStyleInterpolator, - title: route.params?.title, - })} - /> - ({ - cardStyleInterpolator: settingsCardStyleInterpolator, - title: route.params?.title, - })} - /> - ({ - cardStyleInterpolator: settingsCardStyleInterpolator, - title: route.params?.title, - })} - /> - ({ - cardStyleInterpolator: settingsCardStyleInterpolator, - title: route.params?.title, - })} - /> - - - )} - - + + {Object.values(SettingsPages).map( + ({ component, getTitle, key }) => + component && ( + + ) + )} + ({ + cardStyleInterpolator: settingsCardStyleInterpolator, + title: route.params?.title, + })} + /> + ({ + cardStyleInterpolator: settingsCardStyleInterpolator, + title: route.params?.title, + })} + /> + ({ + cardStyleInterpolator: settingsCardStyleInterpolator, + title: route.params?.title, + })} + /> + ({ + cardStyleInterpolator: settingsCardStyleInterpolator, + title: route.params?.title, + })} + /> + ({ + cardStyleInterpolator: settingsCardStyleInterpolator, + title: route.params?.title, + })} + /> + + + )} + ); } diff --git a/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx b/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx index 1b2f4334e8e..ba33ae5da99 100644 --- a/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx +++ b/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx @@ -1,4 +1,3 @@ -import { useCreateBackupStateType } from '@/components/backup/useCreateBackup'; import { useTheme } from '@/theme'; import React, { useState, useMemo, useEffect } from 'react'; import * as i18n from '@/languages'; @@ -6,102 +5,103 @@ import MenuItem from '../MenuItem'; import Spinner from '@/components/Spinner'; import { FloatingEmojis } from '@/components/floating-emojis'; import { useDimensions } from '@/hooks'; +import { CloudBackupState } from '@/state/backups/backups'; export const BackUpMenuItem = ({ icon = '􀊯', - loading, + backupState, onPress, title, + disabled, }: { icon?: string; - loading: useCreateBackupStateType; + backupState: CloudBackupState; title: string; onPress: () => void; + disabled?: boolean; }) => { const { colors } = useTheme(); const { width: deviceWidth } = useDimensions(); const [emojiTrigger, setEmojiTrigger] = useState void)>(null); useEffect(() => { - if (loading === 'success') { + if (backupState === CloudBackupState.Success) { for (let i = 0; i < 20; i++) { setTimeout(() => { emojiTrigger?.(); }, 100 * i); } } - }, [emojiTrigger, loading]); + }, [emojiTrigger, backupState]); const accentColor = useMemo(() => { - switch (loading) { - case 'success': + switch (backupState) { + case CloudBackupState.Success: return colors.green; - case 'error': + case CloudBackupState.Error: return colors.red; default: return undefined; } - }, [colors, loading]); + }, [colors, backupState]); const titleText = useMemo(() => { - switch (loading) { - case 'loading': + switch (backupState) { + case CloudBackupState.InProgress: return i18n.t(i18n.l.back_up.cloud.backing_up); - case 'success': + case CloudBackupState.Success: return i18n.t(i18n.l.back_up.cloud.backup_success); - case 'error': + case CloudBackupState.Error: return i18n.t(i18n.l.back_up.cloud.backup_failed); default: return title; } - }, [loading, title]); + }, [backupState, title]); const localIcon = useMemo(() => { - switch (loading) { - case 'success': + switch (backupState) { + case CloudBackupState.Success: return '􀁢'; - case 'error': + case CloudBackupState.Error: return '􀀲'; default: return icon; } - }, [icon, loading]); + }, [icon, backupState]); return ( - <> - {/* @ts-ignore js */} - - {({ onNewEmoji }: { onNewEmoji: () => void }) => ( - - ) : ( - - ) - } - onPress={() => { - setEmojiTrigger(() => onNewEmoji); - onPress(); - }} - size={52} - titleComponent={} - /> - )} - - + + {({ onNewEmoji }) => ( + + ) : ( + + ) + } + onPress={() => { + setEmojiTrigger(() => onNewEmoji); + onPress(); + }} + size={52} + titleComponent={} + /> + )} + ); }; diff --git a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx index 1842d3fae2a..90cbdddeff3 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx @@ -5,19 +5,18 @@ import { Text as RNText } from '@/components/text'; import Menu from '../Menu'; import MenuContainer from '../MenuContainer'; import MenuItem from '../MenuItem'; -import { Backup, parseTimestampFromFilename } from '@/model/backup'; +import { BackupFile, parseTimestampFromFilename } from '@/model/backup'; import { format } from 'date-fns'; -import { Stack } from '@/design-system'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; -import { IS_ANDROID } from '@/env'; import walletBackupStepTypes from '@/helpers/walletBackupStepTypes'; -import { useCloudBackups } from '@/components/backup/CloudBackupProvider'; -import { Centered } from '@/components/layout'; +import { Page } from '@/components/layout'; import Spinner from '@/components/Spinner'; import ActivityIndicator from '@/components/ActivityIndicator'; -import { cloudPlatform } from '@/utils/platform'; import { useTheme } from '@/theme'; +import { CloudBackupState, LoadingStates, backupsStore } from '@/state/backups/backups'; +import { titleForBackupState } from '../../utils'; +import { Box } from '@/design-system'; const LoadingText = styled(RNText).attrs(({ theme: { colors } }: any) => ({ color: colors.blueGreyDark, @@ -32,43 +31,14 @@ const ViewCloudBackups = () => { const { navigate } = useNavigation(); const { colors } = useTheme(); - const { isFetching, backups } = useCloudBackups(); - - const cloudBackups = backups.files - .filter(backup => { - if (IS_ANDROID) { - return !backup.name.match(/UserData/i); - } - - return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i); - }) - .sort((a, b) => { - return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); - }); - - const mostRecentBackup = cloudBackups.reduce( - (prev, current) => { - if (!current) { - return prev; - } - - if (!prev) { - return current; - } - - const prevTimestamp = new Date(prev.lastModified).getTime(); - const currentTimestamp = new Date(current.lastModified).getTime(); - if (currentTimestamp > prevTimestamp) { - return current; - } - - return prev; - }, - undefined as Backup | undefined - ); + const { status, backups, mostRecentBackup } = backupsStore(state => ({ + status: state.status, + backups: state.backups, + mostRecentBackup: state.mostRecentBackup, + })); const onSelectCloudBackup = useCallback( - async (selectedBackup: Backup) => { + async (selectedBackup: BackupFile) => { navigate(Routes.BACKUP_SHEET, { step: walletBackupStepTypes.restore_from_backup, selectedBackup, @@ -77,80 +47,110 @@ const ViewCloudBackups = () => { [navigate] ); - return ( - - - {!isFetching && !cloudBackups.length && ( - - } /> - - )} + const renderNoBackupsState = () => ( + <> + + } /> + + + ); + + const renderMostRecentBackup = () => { + if (!mostRecentBackup) { + return null; + } + + return ( + + + } + onPress={() => onSelectCloudBackup(mostRecentBackup)} + size={52} + width="full" + titleComponent={} + /> + + + ); + }; + + const renderOlderBackups = () => ( + <> + + + {backups.files + .filter(backup => backup.name !== mostRecentBackup?.name) + .sort((a, b) => { + const timestampA = new Date(parseTimestampFromFilename(a.name)).getTime(); + const timestampB = new Date(parseTimestampFromFilename(b.name)).getTime(); + return timestampB - timestampA; + }) + .map(backup => ( + onSelectCloudBackup(backup)} + size={52} + width="full" + titleComponent={ + + } + /> + ))} + {backups.files.length === 1 && ( + } + /> + )} + + + + + backupsStore.getState().syncAndFetchBackups()} + titleComponent={} + /> + + + ); - {!isFetching && cloudBackups.length && ( - <> - {mostRecentBackup && ( - - } - onPress={() => onSelectCloudBackup(mostRecentBackup)} - size={52} - width="full" - titleComponent={} - /> - - )} + const renderBackupsList = () => ( + <> + {renderMostRecentBackup()} + {renderOlderBackups()} + + ); - - {cloudBackups.map( - backup => - backup.name !== mostRecentBackup?.name && ( - onSelectCloudBackup(backup)} - size={52} - width="full" - titleComponent={ - - } - /> - ) - )} + const isLoading = LoadingStates.includes(status); - {cloudBackups.length === 1 && ( - } - /> - )} - - - )} + if (isLoading) { + return ( + + {android ? : } + {titleForBackupState[status]} + + ); + } - {isFetching && ( - - {android ? : } - { - - {i18n.t(i18n.l.back_up.cloud.fetching_backups, { - cloudPlatformName: cloudPlatform, - })} - - } - - )} - + return ( + + {status === CloudBackupState.Ready && !backups.files.length && renderNoBackupsState()} + {status === CloudBackupState.Ready && backups.files.length > 0 && renderBackupsList()} ); }; diff --git a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx index d085c3f62fd..9fddd15964d 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx @@ -29,31 +29,23 @@ import Routes from '@/navigation/routesNames'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { SETTINGS_BACKUP_ROUTES } from './routes'; import { analyticsV2 } from '@/analytics'; -import { InteractionManager, Linking } from 'react-native'; +import { InteractionManager } from 'react-native'; import { useDispatch } from 'react-redux'; -import { createAccountForWallet, walletsLoadState } from '@/redux/wallets'; -import { - GoogleDriveUserData, - backupUserDataIntoCloud, - getGoogleAccountUserData, - isCloudBackupAvailable, - login, -} from '@/handlers/cloudBackup'; +import { createAccountForWallet } from '@/redux/wallets'; import { logger, RainbowError } from '@/logger'; -import { RainbowAccount, createWallet } from '@/model/wallet'; -import { PROFILES, useExperimentalFlag } from '@/config'; +import { RainbowAccount } from '@/model/wallet'; import showWalletErrorAlert from '@/helpers/support'; -import { IS_ANDROID, IS_IOS } from '@/env'; +import { IS_IOS } from '@/env'; import ImageAvatar from '@/components/contacts/ImageAvatar'; -import { useCreateBackup } from '@/components/backup/useCreateBackup'; import { BackUpMenuItem } from './BackUpMenuButton'; -import { checkWalletsForBackupStatus } from '../../utils'; -import { useCloudBackups } from '@/components/backup/CloudBackupProvider'; -import { WalletCountPerType, useVisibleWallets } from '../../useVisibleWallets'; import { format } from 'date-fns'; import { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; -import { Backup, parseTimestampFromFilename } from '@/model/backup'; -import { WrappedAlert as Alert } from '@/helpers/alert'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; +import { backupsStore } from '@/state/backups/backups'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; +import { isWalletBackedUpForCurrentAccount } from '../../utils'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; type ViewWalletBackupParams = { ViewWalletBackup: { walletId: string; title: string; imported?: boolean }; @@ -126,107 +118,38 @@ const ContextMenuWrapper = ({ children, account, menuConfig, onPressMenuItem }: const ViewWalletBackup = () => { const { params } = useRoute>(); - const { backups } = useCloudBackups(); + const createBackup = useCreateBackup(); + const { status, backupProvider, mostRecentBackup } = backupsStore(state => ({ + status: state.status, + backupProvider: state.backupProvider, + mostRecentBackup: state.mostRecentBackup, + })); const { walletId, title: incomingTitle } = params; const creatingWallet = useRef(); const { isDamaged, wallets } = useWallets(); const wallet = wallets?.[walletId]; const dispatch = useDispatch(); const initializeWallet = useInitializeWallet(); - const profilesEnabled = useExperimentalFlag(PROFILES); - - const walletTypeCount: WalletCountPerType = { - phrase: 0, - privateKey: 0, - }; - - const { lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount }); - - const cloudBackups = backups.files - .filter(backup => { - if (IS_ANDROID) { - return !backup.name.match(/UserData/i); - } - - return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i); - }) - .sort((a, b) => { - return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); - }); - - const mostRecentBackup = cloudBackups.reduce( - (prev, current) => { - if (!current) { - return prev; - } - - if (!prev) { - return current; - } - - const prevTimestamp = new Date(prev.lastModified).getTime(); - const currentTimestamp = new Date(current.lastModified).getTime(); - if (currentTimestamp > prevTimestamp) { - return current; - } - - return prev; - }, - undefined as Backup | undefined - ); - - const { backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); const isSecretPhrase = WalletTypes.mnemonic === wallet?.type; - const title = wallet?.type === WalletTypes.privateKey ? wallet?.addresses[0].label : incomingTitle; + const isBackedUp = isWalletBackedUpForCurrentAccount({ + backupType: wallet?.backupType, + backedUp: wallet?.backedUp, + backupFile: wallet?.backupFile, + }); const { navigate } = useNavigation(); const [isToastActive, setToastActive] = useRecoilState(addressCopiedToastAtom); - const { onSubmit, loading } = useCreateBackup({ - walletId, - }); const backupWalletsToCloud = useCallback(async () => { - if (IS_ANDROID) { - try { - await login(); - - getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { - if (accountDetails) { - return onSubmit({}); - } - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - }); - } catch (e) { - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - logger.error(new RainbowError(`[ViewWalletBackup]: Logging into Google Drive failed`), { error: e }); - } - } else { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - Alert.alert( - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label), - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description), - [ - { - onPress: () => { - Linking.openURL('https://support.apple.com/en-us/HT204025'); - }, - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), - }, - { - style: 'cancel', - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), - }, - ] - ); - return; - } - } - - onSubmit({}); - }, [onSubmit]); + executeFnIfCloudBackupAvailable({ + fn: () => + createBackup({ + walletId, + }), + }); + }, [createBackup, walletId]); const onNavigateToSecretWarning = useCallback(() => { navigate(SETTINGS_BACKUP_ROUTES.SECRET_WARNING, { @@ -265,36 +188,17 @@ const ViewWalletBackup = () => { }, onCloseModal: async (args: any) => { if (args) { + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.CREATING_WALLET, + }); + const name = args?.name ?? ''; const color = args?.color ?? null; // Check if the selected wallet is the primary try { // If we found it and it's not damaged use it to create the new account if (wallet && !wallet.damaged) { - const newWallets = await dispatch(createAccountForWallet(wallet.id, color, name)); - // @ts-expect-error - no params - await initializeWallet(); - // If this wallet was previously backed up to the cloud - // We need to update userData backup so it can be restored too - if (wallet.backedUp && wallet.backupType === walletBackupTypes.cloud) { - try { - await backupUserDataIntoCloud({ wallets: newWallets }); - } catch (e) { - logger.error(new RainbowError(`[ViewWalletBackup]: Updating wallet userdata failed after new account creation`), { - error: e, - }); - throw e; - } - } - - // If doesn't exist, we need to create a new wallet - } else { - await createWallet({ - color, - name, - clearCallbackOnStartCreation: true, - }); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(createAccountForWallet(wallet.id, color, name)); // @ts-expect-error - no params await initializeWallet(); } @@ -307,6 +211,10 @@ const ViewWalletBackup = () => { showWalletErrorAlert(); }, 1000); } + } finally { + walletLoadingStore.setState({ + loadingState: null, + }); } } creatingWallet.current = false; @@ -324,7 +232,7 @@ const ViewWalletBackup = () => { error: e, }); } - }, [creatingWallet, dispatch, isDamaged, navigate, initializeWallet, profilesEnabled, wallet]); + }, [creatingWallet, dispatch, isDamaged, navigate, initializeWallet, wallet]); const handleCopyAddress = React.useCallback( (address: string) => { @@ -386,7 +294,7 @@ const ViewWalletBackup = () => { return ( - {!wallet?.backedUp && ( + {!isBackedUp && ( <> { /> - {backupProvider === walletBackupTypes.cloud && ( + { title={i18n.t(i18n.l.back_up.cloud.back_up_all_wallets_to_cloud, { cloudPlatformName: cloudPlatform, })} - loading={loading} + backupState={status} onPress={backupWalletsToCloud} /> - - )} - - {backupProvider !== walletBackupTypes.cloud && ( - } @@ -456,20 +355,12 @@ const ViewWalletBackup = () => { titleComponent={} testID={'back-up-manually'} /> - - )} + )} - {wallet?.backedUp && ( + {isBackedUp && ( <> { paddingBottom={{ custom: 24 }} iconComponent={ } titleComponent={ { { )} - - } - onPress={onNavigateToSecretWarning} - size={52} - titleComponent={ - + + - } - /> - + + + )} + + + + } + onPress={onNavigateToSecretWarning} + size={52} + titleComponent={ + + } + /> + + {wallet?.addresses .filter(a => a.visible) - .map((account: RainbowAccount) => ( - - } - labelComponent={ - account.label.endsWith('.eth') || account.label !== '' ? ( - - ) : null - } - titleComponent={ - - } - rightComponent={} - /> - - ))} + .map((account: RainbowAccount) => { + const isNamedOrEns = account.label.endsWith('.eth') || removeFirstEmojiFromString(account.label) !== ''; + const label = isNamedOrEns ? abbreviations.address(account.address, 3, 5) : undefined; + const title = isNamedOrEns + ? abbreviations.abbreviateEnsForDisplay(removeFirstEmojiFromString(account.label), 20) ?? '' + : abbreviations.address(account.address, 3, 5) ?? ''; + + return ( + + } + labelComponent={label ? : null} + titleComponent={} + rightComponent={} + /> + + ); + })} {wallet?.type !== WalletTypes.privateKey && ( - - } - onPress={onCreateNewWallet} - size={52} - titleComponent={} - /> - + + + } + onPress={onCreateNewWallet} + size={52} + titleComponent={} + /> + + )} diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 9823fd2555f..74ec4a4e969 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -1,5 +1,4 @@ -/* eslint-disable no-nested-ternary */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; import { cloudPlatform } from '@/utils/platform'; import Menu from '../Menu'; import MenuContainer from '../MenuContainer'; @@ -12,11 +11,11 @@ import WalletTypes, { EthereumWalletType } from '@/helpers/walletTypes'; import ImageAvatar from '@/components/contacts/ImageAvatar'; import { useENSAvatar, useInitializeWallet, useManageCloudBackups, useWallets } from '@/hooks'; import { useNavigation } from '@/navigation'; -import { abbreviations } from '@/utils'; +import { abbreviations, deviceUtils } from '@/utils'; import { addressHashedEmoji } from '@/utils/profileUtils'; import * as i18n from '@/languages'; -import MenuHeader from '../MenuHeader'; -import { checkWalletsForBackupStatus } from '../../utils'; +import MenuHeader, { StatusType } from '../MenuHeader'; +import { checkLocalWalletsForBackupStatus, isWalletBackedUpForCurrentAccount } from '../../utils'; import { Inline, Text, Box, Stack } from '@/design-system'; import { ContactAvatar } from '@/components/contacts'; import { useTheme } from '@/theme'; @@ -25,26 +24,40 @@ import { backupsCard } from '@/components/cards/utils/constants'; import { WalletCountPerType, useVisibleWallets } from '../../useVisibleWallets'; import { SETTINGS_BACKUP_ROUTES } from './routes'; import { RainbowAccount, createWallet } from '@/model/wallet'; -import { PROFILES, useExperimentalFlag } from '@/config'; import { useDispatch } from 'react-redux'; import { walletsLoadState } from '@/redux/wallets'; import { RainbowError, logger } from '@/logger'; import { IS_ANDROID, IS_IOS } from '@/env'; -import { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; import { BackUpMenuItem } from './BackUpMenuButton'; import { format } from 'date-fns'; import { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; -import { Backup, parseTimestampFromFilename } from '@/model/backup'; -import { useCloudBackups } from '@/components/backup/CloudBackupProvider'; -import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup'; -import { WrappedAlert as Alert } from '@/helpers/alert'; -import { Linking } from 'react-native'; -import { noop } from 'lodash'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; +import { AbsolutePortalRoot } from '@/components/AbsolutePortal'; +import { FlatList, ScrollView } from 'react-native'; type WalletPillProps = { account: RainbowAccount; }; +// constants for the account section +const menuContainerPadding = 19.5; // 19px is the padding on the left and right of the container but we need 1px more to account for the shadows on each container +const accountsContainerWidth = deviceUtils.dimensions.width - menuContainerPadding * 4; +const spaceBetweenAccounts = 4; +const accountsItemWidth = accountsContainerWidth / 3; +const basePadding = 16; +const rowHeight = 36; + +const getAccountSectionHeight = (numAccounts: number) => { + const rows = Math.ceil(Math.max(1, numAccounts) / 3); + const paddingBetween = (rows - 1) * 4; + + return basePadding + rows * rowHeight - paddingBetween; +}; + const WalletPill = ({ account }: WalletPillProps) => { const label = useMemo(() => removeFirstEmojiFromString(account.label), [account.label]); @@ -58,7 +71,7 @@ const WalletPill = ({ account }: WalletPillProps) => { key={account.address} flexDirection="row" alignItems="center" - backgroundColor={colors.alpha(colors.grey, 0.4)} + backgroundColor={colors.alpha(colors.grey, 0.24)} borderRadius={23} shadowColor={isDarkMode ? colors.shadow : colors.alpha(colors.blueGreyDark, 0.1)} elevation={12} @@ -67,6 +80,7 @@ const WalletPill = ({ account }: WalletPillProps) => { paddingLeft={{ custom: 4 }} paddingRight={{ custom: 8 }} padding={{ custom: 4 }} + width={{ custom: accountsItemWidth }} > {ENSAvatar?.imageUrl ? ( @@ -82,27 +96,22 @@ const WalletPill = ({ account }: WalletPillProps) => { ); }; -const getAccountSectionHeight = (numAccounts: number) => { - const basePadding = 16; - const rowHeight = 36; - const rows = Math.ceil(Math.max(1, numAccounts) / 3); - const paddingBetween = (rows - 1) * 4; - - return basePadding + rows * rowHeight - paddingBetween; -}; - export const WalletsAndBackup = () => { const { navigate } = useNavigation(); const { wallets } = useWallets(); - const profilesEnabled = useExperimentalFlag(PROFILES); - const { backups } = useCloudBackups(); const dispatch = useDispatch(); - const initializeWallet = useInitializeWallet(); + const scrollviewRef = useRef(null); - const { onSubmit, loading } = useCreateBackup({ - walletId: undefined, // NOTE: This is not used when backing up All wallets - }); + const createBackup = useCreateBackup(); + const { status, backupProvider, backups, mostRecentBackup } = backupsStore(state => ({ + status: state.status, + backupProvider: state.backupProvider, + backups: state.backups, + mostRecentBackup: state.mostRecentBackup, + })); + + const initializeWallet = useInitializeWallet(); const { manageCloudBackups } = useManageCloudBackups(); @@ -111,52 +120,15 @@ export const WalletsAndBackup = () => { privateKey: 0, }; - const { allBackedUp, backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); + const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets, backups), [wallets, backups]); - const { visibleWallets, lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount }); - - const cloudBackups = backups.files - .filter(backup => { - if (IS_ANDROID) { - return !backup.name.match(/UserData/i); - } - - return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i); - }) - .sort((a, b) => { - return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); - }); - - const mostRecentBackup = cloudBackups.reduce( - (prev, current) => { - if (!current) { - return prev; - } - - if (!prev) { - return current; - } - - const prevTimestamp = new Date(prev.lastModified).getTime(); - const currentTimestamp = new Date(current.lastModified).getTime(); - if (currentTimestamp > prevTimestamp) { - return current; - } - - return prev; - }, - undefined as Backup | undefined - ); + const visibleWallets = useVisibleWallets({ wallets, walletTypeCount }); const sortedWallets = useMemo(() => { - const notBackedUpSecretPhraseWallets = visibleWallets.filter( - wallet => !wallet.isBackedUp && wallet.type === EthereumWalletType.mnemonic - ); - const notBackedUpPrivateKeyWallets = visibleWallets.filter( - wallet => !wallet.isBackedUp && wallet.type === EthereumWalletType.privateKey - ); - const backedUpSecretPhraseWallets = visibleWallets.filter(wallet => wallet.isBackedUp && wallet.type === EthereumWalletType.mnemonic); - const backedUpPrivateKeyWallets = visibleWallets.filter(wallet => wallet.isBackedUp && wallet.type === EthereumWalletType.privateKey); + const notBackedUpSecretPhraseWallets = visibleWallets.filter(wallet => !wallet.backedUp && wallet.type === EthereumWalletType.mnemonic); + const notBackedUpPrivateKeyWallets = visibleWallets.filter(wallet => !wallet.backedUp && wallet.type === EthereumWalletType.privateKey); + const backedUpSecretPhraseWallets = visibleWallets.filter(wallet => wallet.backedUp && wallet.type === EthereumWalletType.mnemonic); + const backedUpPrivateKeyWallets = visibleWallets.filter(wallet => wallet.backedUp && wallet.type === EthereumWalletType.privateKey); return [ ...notBackedUpSecretPhraseWallets, @@ -166,48 +138,28 @@ export const WalletsAndBackup = () => { ]; }, [visibleWallets]); - const backupAllNonBackedUpWalletsTocloud = useCallback(async () => { - if (IS_ANDROID) { - try { - await login(); - - getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { - if (accountDetails) { - return onSubmit({ type: BackupTypes.All }); + const backupAllNonBackedUpWalletsTocloud = useCallback(() => { + executeFnIfCloudBackupAvailable({ + fn: () => createBackup({}), + }); + }, [createBackup]); + + const enableCloudBackups = useCallback(() => { + executeFnIfCloudBackupAvailable({ + fn: async () => { + // NOTE: For Android we could be coming from a not-logged-in state, so we + // need to check if we have any wallets to back up first. + if (IS_ANDROID) { + const currentBackups = backupsStore.getState().backups; + if (checkLocalWalletsForBackupStatus(wallets, currentBackups).allBackedUp) { + return; } - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - }); - } catch (e) { - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - logger.error(new RainbowError(`[WalletsAndBackup]: Logging into Google Drive failed`), { - error: e, - }); - } - } else { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - Alert.alert( - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label), - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description), - [ - { - onPress: () => { - Linking.openURL('https://support.apple.com/en-us/HT204025'); - }, - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), - }, - { - style: 'cancel', - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), - }, - ] - ); - return; - } - } - - onSubmit({ type: BackupTypes.All }); - }, [onSubmit]); + } + return createBackup({}); + }, + logout: true, + }); + }, [createBackup, wallets]); const onViewCloudBackups = useCallback(async () => { navigate(SETTINGS_BACKUP_ROUTES.VIEW_CLOUD_BACKUPS, { @@ -223,13 +175,17 @@ export const WalletsAndBackup = () => { onCloseModal: async ({ name }: { name: string }) => { const nameValue = name.trim() !== '' ? name.trim() : ''; try { + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.CREATING_WALLET, + }); + await createWallet({ color: null, name: nameValue, clearCallbackOnStartCreation: true, }); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); // @ts-expect-error - no params await initializeWallet(); @@ -237,10 +193,15 @@ export const WalletsAndBackup = () => { logger.error(new RainbowError(`[WalletsAndBackup]: Failed to create new secret phrase`), { error: err, }); + } finally { + walletLoadingStore.setState({ + loadingState: null, + }); + scrollviewRef.current?.scrollTo({ y: 0, animated: true }); } }, }); - }, [dispatch, initializeWallet, navigate, profilesEnabled, walletTypeCount.phrase]); + }, [dispatch, initializeWallet, navigate, walletTypeCount.phrase]); const onPressLearnMoreAboutCloudBackups = useCallback(() => { navigate(Routes.LEARN_WEB_VIEW_SCREEN, { @@ -263,6 +224,66 @@ export const WalletsAndBackup = () => { [navigate, wallets] ); + const { status: iconStatusType, text } = useMemo<{ status: StatusType; text: string }>(() => { + if (!backupProvider) { + if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) { + return { + status: 'not-enabled', + text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled), + }; + } + + if (status !== CloudBackupState.Ready) { + return { + status: 'out-of-sync', + text: i18n.t(i18n.l.back_up.cloud.statuses.syncing), + }; + } + + if (!allBackedUp) { + return { + status: 'out-of-date', + text: i18n.t(i18n.l.back_up.cloud.statuses.out_of_date), + }; + } + + return { + status: 'up-to-date', + text: i18n.t(i18n.l.back_up.cloud.statuses.up_to_date), + }; + } + + if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) { + return { + status: 'not-enabled', + text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled), + }; + } + + if (status !== CloudBackupState.Ready) { + return { + status: 'out-of-sync', + text: i18n.t(i18n.l.back_up.cloud.statuses.syncing), + }; + } + + if (!allBackedUp) { + return { + status: 'out-of-date', + text: i18n.t(i18n.l.back_up.cloud.statuses.out_of_date), + }; + } + + return { + status: 'up-to-date', + text: i18n.t(i18n.l.back_up.cloud.statuses.up_to_date), + }; + }, [backupProvider, status, allBackedUp]); + + const isCloudBackupDisabled = useMemo(() => { + return status !== CloudBackupState.Ready && status !== CloudBackupState.NotAvailable; + }, [status]); + const renderView = useCallback(() => { switch (backupProvider) { default: @@ -275,7 +296,7 @@ export const WalletsAndBackup = () => { paddingTop={{ custom: 8 }} iconComponent={} titleComponent={} - statusComponent={} + statusComponent={} labelComponent={ { /> - - - + + + + + - {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => { + {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => { + const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType }); + return ( - + { } > - {!backedUp && ( + {!isBackedUp && ( @@ -330,37 +356,43 @@ export const WalletsAndBackup = () => { {imported && } 1 + addresses.length > 1 ? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, { - numAccounts, + numAccounts: addresses.length, }) : i18n.t(i18n.l.wallet.back_ups.wallet_count, { - numAccounts, + numAccounts: addresses.length, }) } /> } leftComponent={} - onPress={() => onNavigateToWalletView(key, name)} + onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} /> - {accounts.map(account => ( - - ))} - + } + keyExtractor={item => item.address} + numColumns={3} + scrollEnabled={false} + /> } /> ); })} + { titleComponent={} /> - - - } - onPress={onViewCloudBackups} - size={52} - titleComponent={ - - } - /> - } - onPress={manageCloudBackups} - size={52} - titleComponent={ - - } - /> - ); @@ -416,12 +417,7 @@ export const WalletsAndBackup = () => { paddingTop={{ custom: 8 }} iconComponent={} titleComponent={} - statusComponent={ - - } + statusComponent={} labelComponent={ allBackedUp ? ( { /> - + - - + } + > + + + - {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => { + {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => { + const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType }); + return ( - + { } > - {!backedUp && } + {!isBackedUp && ( + + )} {imported && } 1 + addresses.length > 1 ? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, { - numAccounts, + numAccounts: addresses.length, }) : i18n.t(i18n.l.wallet.back_ups.wallet_count, { - numAccounts, + numAccounts: addresses.length, }) } /> } leftComponent={} - onPress={() => onNavigateToWalletView(key, name)} + onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} /> - {accounts.map(account => ( - - ))} - + } + keyExtractor={item => item.address} + numColumns={3} + scrollEnabled={false} + /> } /> @@ -581,12 +588,13 @@ export const WalletsAndBackup = () => { case WalletBackupTypes.manual: { return ( - {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => { + {sortedWallets.map(({ id, name, backedUp, backupType, backupFile, imported, addresses }) => { + const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupType, backupFile }); return ( - + { } > - {!backedUp && } + {!isBackedUp && ( + + )} {imported && } 1 + addresses.length > 1 ? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, { - numAccounts, + numAccounts: addresses.length, }) : i18n.t(i18n.l.wallet.back_ups.wallet_count, { - numAccounts, + numAccounts: addresses.length, }) } /> } leftComponent={} - onPress={() => onNavigateToWalletView(key, name)} + onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} /> - {accounts.map(account => ( - - ))} - + } + keyExtractor={item => item.address} + numColumns={3} + scrollEnabled={false} + /> } /> @@ -645,26 +664,29 @@ export const WalletsAndBackup = () => { /> - - {i18n.t(i18n.l.wallet.back_ups.cloud_backup_description, { - cloudPlatform, - })} + + + {i18n.t(i18n.l.wallet.back_ups.cloud_backup_description, { + cloudPlatform, + })} - - {' '} - {i18n.t(i18n.l.wallet.back_ups.cloud_backup_link)} + + {' '} + {i18n.t(i18n.l.wallet.back_ups.cloud_backup_link)} + - - } - > - - + } + > + + + ); @@ -672,21 +694,29 @@ export const WalletsAndBackup = () => { } }, [ backupProvider, - loading, - backupAllNonBackedUpWalletsTocloud, + iconStatusType, + text, + status, + isCloudBackupDisabled, + enableCloudBackups, sortedWallets, onCreateNewSecretPhrase, - onViewCloudBackups, - manageCloudBackups, navigate, onNavigateToWalletView, allBackedUp, mostRecentBackup, - lastBackupDate, + backupAllNonBackedUpWalletsTocloud, + onViewCloudBackups, + manageCloudBackups, onPressLearnMoreAboutCloudBackups, ]); - return {renderView()}; + return ( + + + {renderView()} + + ); }; export default WalletsAndBackup; diff --git a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx index b415e1d4d30..10e28e6ebc6 100644 --- a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx +++ b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx @@ -3,14 +3,12 @@ import { getGoogleAccountUserData, GoogleDriveUserData, logoutFromGoogleDrive } import ImageAvatar from '@/components/contacts/ImageAvatar'; import { showActionSheetWithOptions } from '@/utils'; import * as i18n from '@/languages'; -import { clearAllWalletsBackupStatus, updateWalletBackupStatusesBasedOnCloudUserData } from '@/redux/wallets'; -import { useDispatch } from 'react-redux'; import Menu from './Menu'; import MenuItem from './MenuItem'; import { logger, RainbowError } from '@/logger'; +import { backupsStore } from '@/state/backups/backups'; export const GoogleAccountSection: React.FC = () => { - const dispatch = useDispatch(); const [accountDetails, setAccountDetails] = useState(undefined); const [loading, setLoading] = useState(true); @@ -29,12 +27,6 @@ export const GoogleAccountSection: React.FC = () => { }); }, []); - const removeBackupStateFromAllWallets = async () => { - setLoading(true); - await dispatch(clearAllWalletsBackupStatus()); - setLoading(false); - }; - const onGoogleAccountPress = () => { showActionSheetWithOptions( { @@ -49,11 +41,10 @@ export const GoogleAccountSection: React.FC = () => { if (buttonIndex === 0) { logoutFromGoogleDrive(); setAccountDetails(undefined); - removeBackupStateFromAllWallets().then(() => loginToGoogleDrive()); + loginToGoogleDrive(); } else if (buttonIndex === 1) { logoutFromGoogleDrive(); setAccountDetails(undefined); - removeBackupStateFromAllWallets(); } } ); @@ -61,10 +52,10 @@ export const GoogleAccountSection: React.FC = () => { const loginToGoogleDrive = async () => { setLoading(true); - await dispatch(updateWalletBackupStatusesBasedOnCloudUserData()); try { const accountDetails = await getGoogleAccountUserData(); setAccountDetails(accountDetails ?? undefined); + backupsStore.getState().syncAndFetchBackups(); } catch (error) { logger.error(new RainbowError(`[GoogleAccountSection]: Logging into Google Drive failed`), { error: (error as Error).message, diff --git a/src/screens/SettingsSheet/components/MenuContainer.tsx b/src/screens/SettingsSheet/components/MenuContainer.tsx index 500960c55a5..cabb0157fb7 100644 --- a/src/screens/SettingsSheet/components/MenuContainer.tsx +++ b/src/screens/SettingsSheet/components/MenuContainer.tsx @@ -3,13 +3,14 @@ import { ScrollView } from 'react-native'; import { Box, Inset, Space, Stack } from '@/design-system'; interface MenuContainerProps { + scrollviewRef?: React.RefObject; children: React.ReactNode; Footer?: React.ReactNode; testID?: string; space?: Space; } -const MenuContainer = ({ children, testID, Footer, space = '36px' }: MenuContainerProps) => { +const MenuContainer = ({ scrollviewRef, children, testID, Footer, space = '36px' }: MenuContainerProps) => { return ( // ios scroll fix ( ); -type StatusType = 'not-enabled' | 'out-of-date' | 'up-to-date'; +export type StatusType = 'not-enabled' | 'out-of-date' | 'up-to-date' | 'out-of-sync'; interface StatusIconProps { status: StatusType; @@ -87,6 +87,10 @@ const StatusIcon = ({ status, text }: StatusIconProps) => { backgroundColor: isDarkMode ? colors.alpha(colors.blueGreyDark, 0.1) : colors.alpha(colors.blueGreyDark, 0.1), color: isDarkMode ? colors.alpha(colors.blueGreyDark, 0.6) : colors.alpha(colors.blueGreyDark, 0.8), }, + 'out-of-sync': { + backgroundColor: colors.alpha(colors.yellow, 0.2), + color: colors.yellow, + }, 'out-of-date': { backgroundColor: colors.alpha(colors.brightRed, 0.2), color: colors.brightRed, diff --git a/src/screens/SettingsSheet/components/SettingsSection.tsx b/src/screens/SettingsSheet/components/SettingsSection.tsx index 9fae44a89eb..095b88cbb85 100644 --- a/src/screens/SettingsSheet/components/SettingsSection.tsx +++ b/src/screens/SettingsSheet/components/SettingsSection.tsx @@ -28,9 +28,11 @@ import { showActionSheetWithOptions } from '@/utils'; import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; import { SettingsExternalURLs } from '../constants'; -import { capitalizeFirstLetter, checkWalletsForBackupStatus } from '../utils'; +import { checkLocalWalletsForBackupStatus } from '../utils'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { Box } from '@/design-system'; +import { capitalize } from 'lodash'; +import { backupsStore } from '@/state/backups/backups'; interface SettingsSectionProps { onCloseModal: () => void; @@ -59,10 +61,14 @@ const SettingsSection = ({ const isLanguageSelectionEnabled = useExperimentalFlag(LANGUAGE_SETTINGS); const isNotificationsEnabled = useExperimentalFlag(NOTIFICATIONS); + const { backupProvider, backups } = backupsStore(state => ({ + backupProvider: state.backupProvider, + backups: state.backups, + })); + const { isDarkMode, setTheme, colorScheme } = useTheme(); const onSendFeedback = useSendFeedback(); - const { backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); const onPressReview = useCallback(async () => { if (ios) { @@ -85,7 +91,7 @@ const SettingsSection = ({ const onPressLearn = useCallback(() => Linking.openURL(SettingsExternalURLs.rainbowLearn), []); - const { allBackedUp, canBeBackedUp } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); + const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets, backups), [wallets, backups]); const themeMenuConfig = useMemo(() => { return { @@ -170,21 +176,19 @@ const SettingsSection = ({ return ( }> - {canBeBackedUp && ( - } - onPress={onPressBackup} - rightComponent={ - - - - } - size={60} - testID={'backup-section'} - titleComponent={} - /> - )} + } + onPress={onPressBackup} + rightComponent={ + + + + } + size={60} + testID={'backup-section'} + titleComponent={} + /> {isNotificationsEnabled && ( } - rightComponent={{colorScheme ? capitalizeFirstLetter(colorScheme) : ''}} + rightComponent={{colorScheme ? capitalize(colorScheme) : ''}} size={60} testID={`theme-section-${isDarkMode ? 'dark' : 'light'}`} titleComponent={} diff --git a/src/screens/SettingsSheet/useVisibleWallets.ts b/src/screens/SettingsSheet/useVisibleWallets.ts index 64e73aa0929..c677dd738db 100644 --- a/src/screens/SettingsSheet/useVisibleWallets.ts +++ b/src/screens/SettingsSheet/useVisibleWallets.ts @@ -1,9 +1,7 @@ -import { useState } from 'react'; import * as i18n from '@/languages'; import WalletTypes, { EthereumWalletType } from '@/helpers/walletTypes'; -import { DEFAULT_WALLET_NAME, RainbowAccount, RainbowWallet } from '@/model/wallet'; -import walletBackupTypes from '@/helpers/walletBackupTypes'; +import { RainbowWallet } from '@/model/wallet'; type WalletByKey = { [key: string]: RainbowWallet; @@ -19,20 +17,6 @@ export type WalletCountPerType = { privateKey: number; }; -export type AmendedRainbowWallet = RainbowWallet & { - name: string; - isBackedUp: boolean | undefined; - accounts: RainbowAccount[]; - key: string; - label: string; - numAccounts: number; -}; - -type UseVisibleWalletReturnType = { - visibleWallets: AmendedRainbowWallet[]; - lastBackupDate: number | undefined; -}; - export const getTitleForWalletType = (type: EthereumWalletType, walletTypeCount: WalletCountPerType) => { switch (type) { case EthereumWalletType.mnemonic: @@ -48,51 +32,26 @@ export const getTitleForWalletType = (type: EthereumWalletType, walletTypeCount: } }; -const isWalletGroupNamed = (wallet: RainbowWallet) => wallet.name && wallet.name.trim() !== '' && wallet.name !== DEFAULT_WALLET_NAME; - -export const useVisibleWallets = ({ wallets, walletTypeCount }: UseVisibleWalletProps): UseVisibleWalletReturnType => { - const [lastBackupDate, setLastBackupDate] = useState(undefined); - +export const useVisibleWallets = ({ wallets, walletTypeCount }: UseVisibleWalletProps): RainbowWallet[] => { if (!wallets) { - return { - visibleWallets: [], - lastBackupDate, - }; + return []; } - return { - visibleWallets: Object.keys(wallets) - .filter(key => wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) - .map(key => { - const wallet = wallets[key]; - const visibleAccounts = (wallet.addresses || []).filter(a => a.visible); - const totalAccounts = visibleAccounts.length; - - if ( - wallet.backedUp && - wallet.backupDate && - wallet.backupType === walletBackupTypes.cloud && - (!lastBackupDate || Number(wallet.backupDate) > lastBackupDate) - ) { - setLastBackupDate(Number(wallet.backupDate)); - } - - if (wallet.type === WalletTypes.mnemonic) { - walletTypeCount.phrase += 1; - } else if (wallet.type === WalletTypes.privateKey) { - walletTypeCount.privateKey += 1; - } - - return { - ...wallet, - name: isWalletGroupNamed(wallet) ? wallet.name : getTitleForWalletType(wallet.type, walletTypeCount), - isBackedUp: wallet.backedUp, - accounts: visibleAccounts, - key, - label: wallet.name, - numAccounts: totalAccounts, - }; - }), - lastBackupDate, - }; + return Object.keys(wallets) + .filter(key => wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) + .map(key => { + const wallet = wallets[key]; + + if (wallet.type === WalletTypes.mnemonic) { + walletTypeCount.phrase += 1; + } else if (wallet.type === WalletTypes.privateKey) { + walletTypeCount.privateKey += 1; + } + + return { + ...wallet, + name: getTitleForWalletType(wallet.type, walletTypeCount), + addresses: Object.values(wallet.addresses).filter(address => address.visible), + }; + }); }; diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index 08fa3e03e22..0fb1d26faff 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -1,118 +1,121 @@ import WalletBackupTypes from '@/helpers/walletBackupTypes'; import WalletTypes from '@/helpers/walletTypes'; +import { useWallets } from '@/hooks'; +import { isEmpty } from 'lodash'; +import { BackupFile, CloudBackups, parseTimestampFromFilename } from '@/model/backup'; +import * as i18n from '@/languages'; +import { cloudPlatform } from '@/utils/platform'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; import { RainbowWallet } from '@/model/wallet'; -import { Navigation } from '@/navigation'; -import { BackupUserData, getLocalBackupPassword } from '@/model/backup'; -import Routes from '@/navigation/routesNames'; -import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes'; - -type WalletsByKey = { - [key: string]: RainbowWallet; -}; +import { IS_ANDROID, IS_IOS } from '@/env'; +import { normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; type WalletBackupStatus = { allBackedUp: boolean; areBackedUp: boolean; canBeBackedUp: boolean; - backupProvider: string | undefined; }; -export const capitalizeFirstLetter = (str: string) => { - return str.charAt(0).toUpperCase() + str.slice(1); +export const hasManuallyBackedUpWallet = (wallets: ReturnType['wallets']) => { + if (!wallets) return false; + return Object.values(wallets).some(wallet => wallet.backupType === WalletBackupTypes.manual); }; -export const checkUserDataForBackupProvider = (userData?: BackupUserData): { backupProvider: string | undefined } => { - let backupProvider: string | undefined = undefined; - - if (!userData?.wallets) return { backupProvider }; - - Object.values(userData.wallets).forEach(wallet => { - if (wallet.backedUp && wallet.type !== WalletTypes.readOnly) { - if (wallet.backupType === WalletBackupTypes.cloud) { - backupProvider = WalletBackupTypes.cloud; - } else if (backupProvider !== WalletBackupTypes.cloud && wallet.backupType === WalletBackupTypes.manual) { - backupProvider = WalletBackupTypes.manual; - } - } - }); - - return { backupProvider }; -}; - -export const checkWalletsForBackupStatus = (wallets: WalletsByKey | null): WalletBackupStatus => { - if (!wallets) +export const checkLocalWalletsForBackupStatus = ( + wallets: ReturnType['wallets'], + backups: CloudBackups +): WalletBackupStatus => { + if (!wallets || isEmpty(wallets)) { return { allBackedUp: false, areBackedUp: false, canBeBackedUp: false, - backupProvider: undefined, }; + } + + // FOR ANDROID, we need to check if the current google account also has the backup file + if (IS_ANDROID) { + return Object.values(wallets).reduce( + (acc, wallet) => { + const isBackupEligible = wallet.type !== WalletTypes.readOnly && wallet.type !== WalletTypes.bluetooth; + const hasBackupFile = backups.files.some( + file => normalizeAndroidBackupFilename(file.name) === normalizeAndroidBackupFilename(wallet.backupFile ?? '') + ); + + return { + allBackedUp: acc.allBackedUp && hasBackupFile && (wallet.backedUp || !isBackupEligible), + areBackedUp: acc.areBackedUp && hasBackupFile && (wallet.backedUp || !isBackupEligible), + canBeBackedUp: acc.canBeBackedUp && isBackupEligible, + }; + }, + { allBackedUp: true, areBackedUp: true, canBeBackedUp: false } + ); + } + + return Object.values(wallets).reduce( + (acc, wallet) => { + const isBackupEligible = wallet.type !== WalletTypes.readOnly && wallet.type !== WalletTypes.bluetooth; + + return { + allBackedUp: acc.allBackedUp && (wallet.backedUp || !isBackupEligible), + areBackedUp: acc.areBackedUp && (wallet.backedUp || !isBackupEligible || wallet.imported), + canBeBackedUp: acc.canBeBackedUp && isBackupEligible, + }; + }, + { allBackedUp: true, areBackedUp: true, canBeBackedUp: false } + ); +}; - let backupProvider: string | undefined = undefined; - let areBackedUp = true; - let canBeBackedUp = false; - let allBackedUp = true; - - Object.keys(wallets).forEach(key => { - if (wallets[key].backedUp && wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) { - if (wallets[key].backupType === WalletBackupTypes.cloud) { - backupProvider = WalletBackupTypes.cloud; - } else if (backupProvider !== WalletBackupTypes.cloud && wallets[key].backupType === WalletBackupTypes.manual) { - backupProvider = WalletBackupTypes.manual; - } - } +export const getMostRecentCloudBackup = (backups: BackupFile[]) => { + const cloudBackups = backups.sort((a, b) => { + return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); + }); - if (!wallets[key].backedUp && wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) { - allBackedUp = false; + return cloudBackups.reduce((prev, current) => { + if (!current) { + return prev; } - if ( - !wallets[key].backedUp && - wallets[key].type !== WalletTypes.readOnly && - wallets[key].type !== WalletTypes.bluetooth && - !wallets[key].imported - ) { - areBackedUp = false; + if (!prev) { + return current; } - if (wallets[key].type !== WalletTypes.bluetooth && wallets[key].type !== WalletTypes.readOnly) { - canBeBackedUp = true; + const prevTimestamp = new Date(prev.lastModified).getTime(); + const currentTimestamp = new Date(current.lastModified).getTime(); + if (currentTimestamp > prevTimestamp) { + return current; } - }); - return { - allBackedUp, - areBackedUp, - canBeBackedUp, - backupProvider, - }; + + return prev; + }, cloudBackups[0]); }; -export const getWalletsThatNeedBackedUp = (wallets: { [key: string]: RainbowWallet } | null): RainbowWallet[] => { - if (!wallets) return []; - const walletsToBackup: RainbowWallet[] = []; - Object.keys(wallets).forEach(key => { - if ( - !wallets[key].backedUp && - wallets[key].type !== WalletTypes.readOnly && - wallets[key].type !== WalletTypes.bluetooth && - !wallets[key].imported - ) { - walletsToBackup.push(wallets[key]); - } - }); - return walletsToBackup; +export const titleForBackupState: Partial> = { + [CloudBackupState.Initializing]: i18n.t(i18n.l.back_up.cloud.syncing_cloud_store, { + cloudPlatformName: cloudPlatform, + }), + [CloudBackupState.Syncing]: i18n.t(i18n.l.back_up.cloud.syncing_cloud_store, { + cloudPlatformName: cloudPlatform, + }), + [CloudBackupState.Fetching]: i18n.t(i18n.l.back_up.cloud.fetching_backups, { + cloudPlatformName: cloudPlatform, + }), }; -export const fetchBackupPasswordAndNavigate = async () => { - const password = await getLocalBackupPassword(); +export const isWalletBackedUpForCurrentAccount = ({ backupType, backedUp, backupFile }: Partial) => { + if (!backupType || !backupFile) { + return false; + } - return new Promise(resolve => { - return Navigation.handleAction(Routes.BACKUP_SHEET, { - step: WalletBackupStepTypes.backup_cloud, - password, - onSuccess: async (password: string) => { - resolve(password); - }, - }); - }); + if (IS_IOS || backupType === WalletBackupTypes.manual) { + return backedUp; + } + + // NOTE: For Android, we also need to check if the current google account has the matching backup file + if (!backupFile) { + return false; + } + + const backupFiles = backupsStore.getState().backups; + return backupFiles.files.some(file => normalizeAndroidBackupFilename(file.name) === normalizeAndroidBackupFilename(backupFile)); }; diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index acfcbcfb176..bf5fa201570 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -25,11 +25,11 @@ import { RemoteCardsSync } from '@/state/sync/RemoteCardsSync'; import { RemotePromoSheetSync } from '@/state/sync/RemotePromoSheetSync'; import { UserAssetsSync } from '@/state/sync/UserAssetsSync'; import { MobileWalletProtocolListener } from '@/components/MobileWalletProtocolListener'; -import { runWalletBackupStatusChecks } from '@/handlers/walletReadyEvents'; import { RouteProp, useRoute } from '@react-navigation/native'; import { RootStackParamList } from '@/navigation/types'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/Routes'; +import { BackendNetworks } from '@/components/BackendNetworks'; import walletTypes from '@/helpers/walletTypes'; enum WalletLoadingStates { @@ -45,7 +45,6 @@ function WalletScreen() { const walletState = useRef(WalletLoadingStates.IDLE); const initializeWallet = useInitializeWallet(); const { network: currentNetwork, accountAddress, appIcon } = useAccountSettings(); - const loadAccountLateData = useLoadAccountLateData(); const loadGlobalLateData = useLoadGlobalLateData(); const insets = useSafeAreaInsets(); @@ -149,7 +148,6 @@ function WalletScreen() { if (walletReady) { loadAccountLateData(); loadGlobalLateData(); - runWalletBackupStatusChecks(); } }, [loadAccountLateData, loadGlobalLateData, walletReady]); @@ -185,6 +183,7 @@ function WalletScreen() { + {/* NOTE: This component listens for Mobile Wallet Protocol requests and handles them */} diff --git a/src/screens/rewards/components/RewardsEarnings.tsx b/src/screens/rewards/components/RewardsEarnings.tsx index 48e70481cec..b351d4be895 100644 --- a/src/screens/rewards/components/RewardsEarnings.tsx +++ b/src/screens/rewards/components/RewardsEarnings.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { Image } from 'react-native'; +import { Image, ImageBackground } from 'react-native'; import { RewardsSectionCard } from '@/screens/rewards/components/RewardsSectionCard'; import { AccentColorProvider, Box, Columns, Inline, Stack, Text } from '@/design-system'; import * as i18n from '@/languages'; @@ -63,7 +63,7 @@ export const RewardsEarnings: React.FC = ({ airdropTitle, airdropTime, }; - }, [pendingEarningsToken, tokenSymbol, totalEarnings.token, totalEarnings.usd, nextAirdropTimestamp]); + }, [pendingEarningsToken, tokenSymbol, totalEarnings.token, totalEarnings.usd, assetPrice, nativeCurrency, nextAirdropTimestamp]); const navigateToTimingExplainer = () => { analyticsV2.track(analyticsV2.event.rewardsPressedPendingEarningsCard); @@ -85,7 +85,7 @@ export const RewardsEarnings: React.FC = ({ = ({ (backendNetworksQueryKey()) ?? buildTimeNetworks; const DEFAULT_PRIVATE_MEMPOOL_TIMEOUT = 2 * 60 * 1_000; // 2 minutes @@ -19,6 +20,7 @@ export interface BackendNetworksState { getBackendChains: () => Chain[]; getSupportedChains: () => Chain[]; + getSortedSupportedChainIds: () => number[]; getDefaultChains: () => Record; getSupportedChainIds: () => ChainId[]; @@ -31,6 +33,8 @@ export interface BackendNetworksState { getChainsName: () => Record; getChainsIdByName: () => Record; + getColorsForChainId: (chainId: ChainId, isDarkMode: boolean) => string; + defaultGasSpeeds: (chainId: ChainId) => GasSpeed[]; getChainsGasSpeeds: () => Record; @@ -75,6 +79,11 @@ export const useBackendNetworksStore = createRainbowStore( return IS_TEST ? [...backendChains, chainHardhat, chainHardhatOptimism] : backendChains; }, + getSortedSupportedChainIds: () => { + const supportedChains = get().getSupportedChains(); + return supportedChains.sort((a, b) => a.name.localeCompare(b.name)).map(c => c.id); + }, + getDefaultChains: () => { const supportedChains = get().getSupportedChains(); return supportedChains.reduce( @@ -163,6 +172,17 @@ export const useBackendNetworksStore = createRainbowStore( ); }, + getColorsForChainId: (chainId: ChainId, isDarkMode: boolean) => { + const { backendNetworks } = get(); + + const colors = backendNetworks.networks.find(chain => +chain.id === chainId)?.colors; + if (!colors) { + return isDarkMode ? globalColors.white : globalColors.black; + } + + return isDarkMode ? colors.dark : colors.light; + }, + // TODO: This should come from the backend at some point defaultGasSpeeds: chainId => { switch (chainId) { diff --git a/src/state/backendNetworks/types.ts b/src/state/backendNetworks/types.ts index ab84a34b120..bc5ea01c827 100644 --- a/src/state/backendNetworks/types.ts +++ b/src/state/backendNetworks/types.ts @@ -168,6 +168,10 @@ export interface BackendNetwork { id: string; name: string; label: string; + colors: { + light: string; + dark: string; + }; icons: { badgeURL: string; }; diff --git a/src/state/backups/backups.ts b/src/state/backups/backups.ts new file mode 100644 index 00000000000..ef1abf3ab23 --- /dev/null +++ b/src/state/backups/backups.ts @@ -0,0 +1,182 @@ +import { BackupFile, CloudBackups } from '@/model/backup'; +import { createRainbowStore } from '../internal/createRainbowStore'; +import { IS_ANDROID } from '@/env'; +import { fetchAllBackups, getGoogleAccountUserData, isCloudBackupAvailable, syncCloud } from '@/handlers/cloudBackup'; +import { RainbowError, logger } from '@/logger'; +import walletBackupTypes from '@/helpers/walletBackupTypes'; +import { getMostRecentCloudBackup, hasManuallyBackedUpWallet } from '@/screens/SettingsSheet/utils'; +import { Mutex } from 'async-mutex'; +import store from '@/redux/store'; + +const mutex = new Mutex(); + +export enum CloudBackupState { + Initializing = 'initializing', + Syncing = 'syncing', + Fetching = 'fetching', + FailedToInitialize = 'failed_to_initialize', + Ready = 'ready', + NotAvailable = 'not_available', + InProgress = 'in_progress', + Error = 'error', + Success = 'success', +} + +const DEFAULT_TIMEOUT = 10_000; +const MAX_RETRIES = 3; + +export const LoadingStates = [CloudBackupState.Initializing, CloudBackupState.Syncing, CloudBackupState.Fetching]; + +interface BackupsStore { + storedPassword: string; + setStoredPassword: (storedPassword: string) => void; + + backupProvider: string | undefined; + setBackupProvider: (backupProvider: string | undefined) => void; + + status: CloudBackupState; + setStatus: (status: CloudBackupState) => void; + + backups: CloudBackups; + setBackups: (backups: CloudBackups) => void; + + mostRecentBackup: BackupFile | undefined; + setMostRecentBackup: (backup: BackupFile | undefined) => void; + + password: string; + setPassword: (password: string) => void; + + syncAndFetchBackups: ( + retryOnFailure?: boolean, + retryCount?: number + ) => Promise<{ + success: boolean; + retry?: boolean; + }>; +} + +const returnEarlyIfLockedStates = [CloudBackupState.Syncing, CloudBackupState.Fetching]; + +export const backupsStore = createRainbowStore((set, get) => ({ + storedPassword: '', + setStoredPassword: storedPassword => set({ storedPassword }), + + backupProvider: undefined, + setBackupProvider: provider => set({ backupProvider: provider }), + + status: CloudBackupState.Initializing, + setStatus: status => set({ status }), + + backups: { files: [] }, + setBackups: backups => set({ backups }), + + mostRecentBackup: undefined, + setMostRecentBackup: backup => set({ mostRecentBackup: backup }), + + password: '', + setPassword: password => set({ password }), + + syncAndFetchBackups: async (retryOnFailure = true, retryCount = 0) => { + const { status } = get(); + + const timeoutPromise = new Promise<{ success: boolean; retry?: boolean }>(resolve => { + setTimeout(() => { + resolve({ success: false, retry: retryOnFailure }); + }, DEFAULT_TIMEOUT); + }); + + const syncAndPullFiles = async (): Promise<{ success: boolean; retry?: boolean }> => { + try { + const isAvailable = await isCloudBackupAvailable(); + if (!isAvailable) { + logger.debug('[backupsStore]: Cloud backup is not available'); + set({ backupProvider: undefined, status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined }); + return { + success: false, + retry: false, + }; + } + + if (IS_ANDROID) { + const gdata = await getGoogleAccountUserData(); + if (!gdata) { + logger.debug('[backupsStore]: Google account is not available'); + set({ backupProvider: undefined, status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined }); + return { + success: false, + retry: false, + }; + } + } + + set({ status: CloudBackupState.Syncing }); + logger.debug('[backupsStore]: Syncing with cloud'); + await syncCloud(); + + set({ status: CloudBackupState.Fetching }); + logger.debug('[backupsStore]: Fetching backups'); + const backups = await fetchAllBackups(); + + set({ backups }); + + const { wallets } = store.getState().wallets; + + // if the user has any cloud backups, set the provider to cloud + if (backups.files.length > 0) { + set({ + backupProvider: walletBackupTypes.cloud, + mostRecentBackup: getMostRecentCloudBackup(backups.files), + }); + } else if (hasManuallyBackedUpWallet(wallets)) { + set({ backupProvider: walletBackupTypes.manual }); + } else { + set({ backupProvider: undefined }); + } + + logger.debug(`[backupsStore]: Retrieved ${backups.files.length} backup files`); + + set({ status: CloudBackupState.Ready }); + return { + success: true, + retry: false, + }; + } catch (e) { + logger.error(new RainbowError('[backupsStore]: Failed to fetch all backups'), { + error: e, + }); + set({ status: CloudBackupState.FailedToInitialize }); + } + + return { + success: false, + retry: retryOnFailure, + }; + }; + + if (mutex.isLocked() || returnEarlyIfLockedStates.includes(status)) { + logger.debug('[backupsStore]: Mutex is locked or returnEarlyIfLockedStates includes status', { + status, + }); + return { + success: false, + retry: false, + }; + } + + const releaser = await mutex.acquire(); + logger.debug('[backupsStore]: Acquired mutex'); + const { success, retry } = await Promise.race([syncAndPullFiles(), timeoutPromise]); + releaser(); + logger.debug('[backupsStore]: Released mutex'); + if (retry && retryCount < MAX_RETRIES) { + logger.debug(`[backupsStore]: Retrying sync and fetch backups attempt: ${retryCount + 1}`); + return get().syncAndFetchBackups(retryOnFailure, retryCount + 1); + } + + if (retry && retryCount >= MAX_RETRIES) { + logger.error(new RainbowError('[backupsStore]: Max retry attempts reached. Sync failed.')); + } + + return { success, retry }; + }, +})); diff --git a/src/state/internal/createRainbowStore.ts b/src/state/internal/createRainbowStore.ts index 9b7d9a39dd7..e0c14b79ead 100644 --- a/src/state/internal/createRainbowStore.ts +++ b/src/state/internal/createRainbowStore.ts @@ -43,6 +43,12 @@ interface RainbowPersistConfig { * This function will be called when persisted state versions mismatch with the one specified here. */ migrate?: (persistedState: unknown, version: number) => S | Promise; + /** + * A function returning another (optional) function. + * The main function will be called before the state rehydration. + * The returned function will be called after the state rehydration or when an error occurred. + */ + onRehydrateStorage?: PersistOptions>['onRehydrateStorage']; } /** @@ -157,6 +163,7 @@ export function createRainbowStore( storage: persistStorage, version, migrate: persistConfig.migrate, + onRehydrateStorage: persistConfig.onRehydrateStorage, }) ) ); diff --git a/src/state/networkSwitcher/networkSwitcher.ts b/src/state/networkSwitcher/networkSwitcher.ts new file mode 100644 index 00000000000..aa82dc85a44 --- /dev/null +++ b/src/state/networkSwitcher/networkSwitcher.ts @@ -0,0 +1,57 @@ +import { ChainId } from '@/state/backendNetworks/types'; +import { createRainbowStore } from '../internal/createRainbowStore'; +import { analyticsV2 } from '@/analytics'; +import { nonceStore } from '@/state/nonces'; +import { logger } from '@/logger'; + +export const defaultPinnedNetworks = [ChainId.base, ChainId.mainnet, ChainId.optimism, ChainId.arbitrum, ChainId.polygon, ChainId.zora]; + +function getMostUsedChains() { + try { + const noncesByAddress = nonceStore.getState().nonces; + const summedNoncesByChainId: Record = {}; + for (const addressNonces of Object.values(noncesByAddress)) { + for (const [chainId, { currentNonce }] of Object.entries(addressNonces)) { + summedNoncesByChainId[chainId] ??= 0; + summedNoncesByChainId[chainId] += currentNonce || 0; + } + } + + const mostUsedNetworks = Object.entries(summedNoncesByChainId) + .sort((a, b) => b[1] - a[1]) + .map(([chainId]) => parseInt(chainId)); + + return mostUsedNetworks.length ? mostUsedNetworks.slice(0, 5) : defaultPinnedNetworks; + } catch (error) { + logger.warn('[networkSwitcher]: Error getting most used chains', { error }); + return defaultPinnedNetworks; + } +} + +export const networkSwitcherStore = createRainbowStore<{ + pinnedNetworks: ChainId[]; +}>(() => ({ pinnedNetworks: getMostUsedChains().slice(0, 5) }), { + storageKey: 'network-switcher', + version: 0, + onRehydrateStorage(state) { + // if we are missing pinned networks, use the user most used chains + if (state.pinnedNetworks.length === 0) { + const mostUsedNetworks = getMostUsedChains(); + state.pinnedNetworks = mostUsedNetworks.slice(0, 5); + analyticsV2.identify({ mostUsedNetworks: mostUsedNetworks.filter(Boolean) }); + } + }, +}); + +export const customizeNetworksBannerStore = createRainbowStore<{ + dismissedAt: number; // timestamp +}>(() => ({ dismissedAt: 0 }), { + storageKey: 'CustomizeNetworksBanner', + version: 0, +}); + +const twoWeeks = 1000 * 60 * 60 * 24 * 7 * 2; +export const shouldShowCustomizeNetworksBanner = (dismissedAt: number) => Date.now() - dismissedAt > twoWeeks; +export const dismissCustomizeNetworksBanner = () => { + customizeNetworksBannerStore.setState({ dismissedAt: Date.now() }); +}; diff --git a/src/state/swaps/swapsStore.ts b/src/state/swaps/swapsStore.ts index 49cc99d0268..7d4d0d99f4b 100644 --- a/src/state/swaps/swapsStore.ts +++ b/src/state/swaps/swapsStore.ts @@ -1,5 +1,5 @@ import { INITIAL_SLIDER_POSITION } from '@/__swaps__/screens/Swap/constants'; -import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; +import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset, UniqueId } from '@/__swaps__/types/assets'; import { ChainId } from '@/state/backendNetworks/types'; import { RecentSwap } from '@/__swaps__/types/swap'; import { getDefaultSlippage } from '@/__swaps__/utils/swaps'; @@ -42,6 +42,8 @@ export interface SwapsState { // degen mode preferences preferredNetwork: ChainId | undefined; setPreferredNetwork: (preferredNetwork: ChainId | undefined) => void; + + lastNavigatedTrendingToken: UniqueId | undefined; } type StateWithTransforms = Omit, 'latestSwapAt' | 'recentSwaps'> & { @@ -156,6 +158,8 @@ export const swapsStore = createRainbowStore( latestSwapAt: new Map(latestSwapAt), }); }, + + lastNavigatedTrendingToken: undefined, }), { storageKey: 'swapsStore', diff --git a/src/state/sync/BackupsSync.tsx b/src/state/sync/BackupsSync.tsx new file mode 100644 index 00000000000..a409490c205 --- /dev/null +++ b/src/state/sync/BackupsSync.tsx @@ -0,0 +1,12 @@ +import { useEffect, memo } from 'react'; +import { backupsStore } from '@/state/backups/backups'; + +const BackupsSyncComponent = () => { + useEffect(() => { + backupsStore.getState().syncAndFetchBackups(); + }, []); + + return null; +}; + +export const BackupsSync = memo(BackupsSyncComponent); diff --git a/src/state/trendingTokens/trendingTokens.ts b/src/state/trendingTokens/trendingTokens.ts new file mode 100644 index 00000000000..514160352d1 --- /dev/null +++ b/src/state/trendingTokens/trendingTokens.ts @@ -0,0 +1,59 @@ +import { analyticsV2 } from '@/analytics'; +import { ChainId } from '@/state/backendNetworks/types'; +import { createRainbowStore } from '../internal/createRainbowStore'; +import { + TrendingCategory as ArcTrendingCategory, + Timeframe as ArcTimeframe, + TrendingSort as ArcTrendingSort, +} from '@/graphql/__generated__/arc'; + +export const categories = [ArcTrendingCategory.Trending, ArcTrendingCategory.New, ArcTrendingCategory.Farcaster] as const; +export type TrendingCategory = (typeof categories)[number]; +export const sortFilters = [ + ArcTrendingSort.Recommended, + ArcTrendingSort.Volume, + ArcTrendingSort.MarketCap, + ArcTrendingSort.TopGainers, + ArcTrendingSort.TopLosers, +] as const; +export type TrendingSort = (typeof sortFilters)[number]; +export const timeFilters = [ArcTimeframe.H12, ArcTimeframe.H24, ArcTimeframe.D3, ArcTimeframe.D7] as const; +export type TrendingTimeframe = (typeof timeFilters)[number]; + +type TrendingTokensState = { + category: (typeof categories)[number]; + chainId: undefined | ChainId; + timeframe: (typeof timeFilters)[number]; + sort: (typeof sortFilters)[number]; + + setCategory: (category: TrendingTokensState['category']) => void; + setChainId: (chainId: TrendingTokensState['chainId']) => void; + setTimeframe: (timeframe: TrendingTokensState['timeframe']) => void; + setSort: (sort: TrendingTokensState['sort']) => void; +}; + +export const useTrendingTokensStore = createRainbowStore( + set => ({ + category: ArcTrendingCategory.Trending, + chainId: undefined, + timeframe: ArcTimeframe.D3, + sort: ArcTrendingSort.Recommended, + setCategory: category => set({ category }), + setChainId: chainId => { + analyticsV2.track(analyticsV2.event.changeNetworkFilter, { chainId }); + set({ chainId }); + }, + setTimeframe: timeframe => { + analyticsV2.track(analyticsV2.event.changeTimeframeFilter, { timeframe }); + set({ timeframe }); + }, + setSort: sort => { + analyticsV2.track(analyticsV2.event.changeSortFilter, { sort }); + set({ sort }); + }, + }), + { + storageKey: 'trending-tokens', + version: 1, + } +); diff --git a/src/state/walletLoading/walletLoading.ts b/src/state/walletLoading/walletLoading.ts new file mode 100644 index 00000000000..7391b78e760 --- /dev/null +++ b/src/state/walletLoading/walletLoading.ts @@ -0,0 +1,18 @@ +import { createRainbowStore } from '../internal/createRainbowStore'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; + +type WalletLoadingState = { + loadingState: WalletLoadingStates | null; + blockTouches: boolean; + Component: JSX.Element | null; + hide: () => void; + setComponent: (Component: JSX.Element, blockTouches?: boolean) => void; +}; + +export const walletLoadingStore = createRainbowStore(set => ({ + loadingState: null, + blockTouches: false, + Component: null, + hide: () => set({ blockTouches: false, Component: null }), + setComponent: (Component: JSX.Element, blockTouches = true) => set({ blockTouches, Component }), +})); diff --git a/src/styles/colors.ts b/src/styles/colors.ts index 04d23086e48..2deaa5ba2f4 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { globalColors } from '@/design-system'; import currentColors from '../theme/currentColors'; import { memoFn } from '../utils/memoFn'; -import { ChainId } from '@/state/backendNetworks/types'; export type Colors = ReturnType; @@ -186,28 +185,6 @@ const getColorsByTheme = (darkMode?: boolean) => { }, }; - let networkColors = { - [ChainId.zksync]: '#25292E', - [ChainId.sanko]: '#594BA7', - [ChainId.scroll]: '#A6855D', - [ChainId.linea]: '#25292E', - [ChainId.gravity]: '#B75E2C', - [ChainId.ink]: '#7132F5', - [ChainId.arbitrum]: '#2D374B', - [ChainId.base]: '#0052FF', - [ChainId.goerli]: '#f6c343', - [ChainId.gnosis]: '#133629', - [ChainId.mainnet]: '#25292E', - [ChainId.optimism]: '#FF4040', - [ChainId.polygon]: '#8247E5', - [ChainId.bsc]: '#F0B90B', - [ChainId.zora]: '#2B5DF0', - [ChainId.avalanche]: '#E84142', - [ChainId.degen]: '#A36EFD', - [ChainId.blast]: '#25292E', - [ChainId.apechain]: '#0054FA', - }; - let gradients = { appleBlueTintToAppleBlue: ['#15B1FE', base.appleBlue], blueToGreen: ['#4764F7', '#23D67F'], @@ -334,28 +311,6 @@ const getColorsByTheme = (darkMode?: boolean) => { secondGradient: '#12131A80', thirdGradient: '#12131Aff', }; - - networkColors = { - [ChainId.zksync]: '#FFFFFF', - [ChainId.sanko]: '#7F6FC9', - [ChainId.scroll]: '#EBC28E', - [ChainId.linea]: '#FFFFFF', - [ChainId.gravity]: '#B75E2C', - [ChainId.ink]: '#864DFF', - [ChainId.arbitrum]: '#ADBFE3', - [ChainId.base]: '#3979FF', - [ChainId.goerli]: '#f6c343', - [ChainId.gnosis]: '#F0EBDE', - [ChainId.mainnet]: '#E0E8FF', - [ChainId.optimism]: '#FF6A6A', - [ChainId.polygon]: '#A275EE', - [ChainId.bsc]: '#F0B90B', - [ChainId.zora]: '#6183F0', - [ChainId.avalanche]: '#FF5D5E', - [ChainId.degen]: '#A36EFD', - [ChainId.blast]: '#FCFC03', - [ChainId.apechain]: '#397BFF', - }; } return { @@ -370,7 +325,6 @@ const getColorsByTheme = (darkMode?: boolean) => { isColorDark, isColorLight, listHeaders, - networkColors, sendScreen, ...base, ...transparent, diff --git a/src/walletConnect/sheets/AuthRequest.tsx b/src/walletConnect/sheets/AuthRequest.tsx index 338acad39b4..724cae00de2 100644 --- a/src/walletConnect/sheets/AuthRequest.tsx +++ b/src/walletConnect/sheets/AuthRequest.tsx @@ -149,7 +149,7 @@ export function AuthRequest({ navigate(Routes.CHANGE_WALLET_SHEET, { watchOnly: true, currentAccountAddress: address, - onChangeWallet(address: string) { + onChangeWallet(address) { setAddress(address); goBack(); },