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 && (
)}
- {!isFetching && !cloudBackups.length && step !== CloudBackupStep.FAILED && (
+ {status === CloudBackupState.Ready && backups.files.length === 0 && (
+
+
+
+
)}
- {!isFetching && cloudBackups.length > 0 && (
+ {status === CloudBackupState.Ready && backups.files.length > 0 && (
{mostRecentBackup && (
-
+
+
+
)}
-
+
)}
- {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 => (
+
+
+
+
+ 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();
},