diff --git a/package-lock.json b/package-lock.json
index f50889c3e53..c2426d11b12 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5928,6 +5928,7 @@
},
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0",
+ "extraneous": true,
"inBundle": true,
"license": "MIT",
"engines": {
@@ -146304,7 +146305,7 @@
},
"packages/mobile": {
"name": "@audius/mobile",
- "version": "1.5.169",
+ "version": "1.5.170",
"dependencies": {
"@amplitude/analytics-react-native": "1.4.11",
"@audius/common": "*",
diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock
index 2f788cd6add..2ce7ca509c2 100644
--- a/packages/mobile/ios/Podfile.lock
+++ b/packages/mobile/ios/Podfile.lock
@@ -18,7 +18,7 @@ PODS:
- ffmpeg-kit-ios-full-gpl (= 6.0)
- React-Core
- FingerprintPro (2.11.0)
- - fmt (11.0.2)
+ - fmt (12.1.0)
- glog (0.3.5)
- google-cast-sdk-dynamic-xcframework-no-bluetooth (4.7.1)
- hermes-engine (0.78.3):
@@ -55,20 +55,20 @@ PODS:
- boost
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- RCT-Folly/Default (= 2024.11.18.00)
- RCT-Folly/Default (2024.11.18.00):
- boost
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- RCT-Folly/Fabric (2024.11.18.00):
- boost
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- RCTDeprecation (0.78.3)
- RCTRequired (0.78.3)
@@ -331,7 +331,7 @@ PODS:
- React-CoreModules (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- RCT-Folly (= 2024.11.18.00)
- RCTTypeSafety (= 0.78.3)
- React-Core/CoreModulesHeaders (= 0.78.3)
@@ -347,7 +347,7 @@ PODS:
- boost
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
@@ -384,7 +384,7 @@ PODS:
- React-Fabric (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -421,7 +421,7 @@ PODS:
- React-Fabric/animations (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -442,7 +442,7 @@ PODS:
- React-Fabric/attributedstring (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -463,7 +463,7 @@ PODS:
- React-Fabric/componentregistry (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -484,7 +484,7 @@ PODS:
- React-Fabric/componentregistrynative (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -505,7 +505,7 @@ PODS:
- React-Fabric/components (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -529,7 +529,7 @@ PODS:
- React-Fabric/components/legacyviewmanagerinterop (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -550,7 +550,7 @@ PODS:
- React-Fabric/components/root (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -571,7 +571,7 @@ PODS:
- React-Fabric/components/view (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -593,7 +593,7 @@ PODS:
- React-Fabric/consistency (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -614,7 +614,7 @@ PODS:
- React-Fabric/core (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -635,7 +635,7 @@ PODS:
- React-Fabric/dom (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -656,7 +656,7 @@ PODS:
- React-Fabric/imagemanager (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -677,7 +677,7 @@ PODS:
- React-Fabric/leakchecker (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -698,7 +698,7 @@ PODS:
- React-Fabric/mounting (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -719,7 +719,7 @@ PODS:
- React-Fabric/observers (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -741,7 +741,7 @@ PODS:
- React-Fabric/observers/events (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -762,7 +762,7 @@ PODS:
- React-Fabric/scheduler (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -785,7 +785,7 @@ PODS:
- React-Fabric/telemetry (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -806,7 +806,7 @@ PODS:
- React-Fabric/templateprocessor (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -827,7 +827,7 @@ PODS:
- React-Fabric/uimanager (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -850,7 +850,7 @@ PODS:
- React-Fabric/uimanager/consistency (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -872,7 +872,7 @@ PODS:
- React-FabricComponents (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -897,7 +897,7 @@ PODS:
- React-FabricComponents/components (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -929,7 +929,7 @@ PODS:
- React-FabricComponents/components/inputaccessory (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -952,7 +952,7 @@ PODS:
- React-FabricComponents/components/iostextinput (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -975,7 +975,7 @@ PODS:
- React-FabricComponents/components/modal (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -998,7 +998,7 @@ PODS:
- React-FabricComponents/components/rncore (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -1021,7 +1021,7 @@ PODS:
- React-FabricComponents/components/safeareaview (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -1044,7 +1044,7 @@ PODS:
- React-FabricComponents/components/scrollview (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -1067,7 +1067,7 @@ PODS:
- React-FabricComponents/components/text (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -1090,7 +1090,7 @@ PODS:
- React-FabricComponents/components/textinput (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -1113,7 +1113,7 @@ PODS:
- React-FabricComponents/components/unimplementedview (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -1136,7 +1136,7 @@ PODS:
- React-FabricComponents/textlayoutmanager (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -1159,7 +1159,7 @@ PODS:
- React-FabricImage (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -1189,7 +1189,7 @@ PODS:
- React-graphics (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly/Fabric (= 2024.11.18.00)
@@ -1199,7 +1199,7 @@ PODS:
- React-hermes (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
@@ -1240,14 +1240,14 @@ PODS:
- boost
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- React-jsiexecutor (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
@@ -1337,7 +1337,7 @@ PODS:
- React
- react-native-google-cast/NoBluetoothArm (= 4.6.2)
- react-native-google-cast/NoBluetoothArm (4.6.2):
- - google-cast-sdk-dynamic-xcframework-no-bluetooth (= 4.7.1)
+ - google-cast-sdk-dynamic-xcframework-no-bluetooth
- React
- react-native-google-cast/RNGoogleCast
- react-native-google-cast/RNGoogleCast (4.6.2):
@@ -1567,7 +1567,7 @@ PODS:
- React-RCTBlob (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- React-Core/RCTBlobHeaders
@@ -1657,7 +1657,7 @@ PODS:
- React-rendererdebug (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- RCT-Folly (= 2024.11.18.00)
- React-debug
- React-rncore (0.78.3)
@@ -1759,7 +1759,7 @@ PODS:
- ReactCommon/turbomodule (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
@@ -1773,7 +1773,7 @@ PODS:
- ReactCommon/turbomodule/bridging (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
@@ -1785,7 +1785,7 @@ PODS:
- ReactCommon/turbomodule/core (0.78.3):
- DoubleConversion
- fast_float (= 6.1.4)
- - fmt (= 11.0.2)
+ - fmt (= 12.1.0)
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
@@ -2503,7 +2503,7 @@ SPEC CHECKSUMS:
ffmpeg-kit-ios-full-gpl: 7d416729f7b3604b64cee2a752c42608a8d228e0
ffmpeg-kit-react-native: 3cea88c9c5cfad62e1465279ea7d800dfbba3b00
FingerprintPro: 2fbd0cda75fbab32055f7cb38bc5fc9d17168979
- fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
+ fmt: 530618a01105dae0fa3a2f27c81ae11fa8f67eac
glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
google-cast-sdk-dynamic-xcframework-no-bluetooth: 1fa9e267df3fd6f8a1c6e3345142ca5286297968
hermes-engine: b5c9cfbe6415f1b0b24759f2942c8f33e9af6347
@@ -2512,30 +2512,30 @@ SPEC CHECKSUMS:
lottie-react-native: 04061d06c966a4179c9c1352aac63b699642c77e
nSure: 2fc3fc973c44aa0be9a3446f84cb514adc475205
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
- RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
+ RCT-Folly: d533c1d21249a85e67a4be97f4d793665de0222e
RCTDeprecation: cf39863b43871c2031050605fb884019b6193910
RCTRequired: 8fdd66f4a97f352b66f38cfef13fc11b12d2c884
RCTTypeSafety: c9c9e64389bc545fc137030615b387ef1654dcee
React: 14a80ea4f13387cfdaa4250b46fbfe19754c220c
React-callinvoker: fed1dad5d6cf992c7b4b5fdbf1bf67fe2e8fb6c5
React-Core: f703e7a56fcedc3e959b8b7899995e57fd58539a
- React-CoreModules: 6e87c904cc257058c271708eef1719b5b3039131
- React-cxxreact: 4153beeff710944832cd90ccb141e299ee16b7d3
+ React-CoreModules: 945ed8a2827720af6852b97c8ddd2ac2ceb04157
+ React-cxxreact: 26c744a099a29f2f995f9bb4b755d8ecbf7c5392
React-debug: aea2894f3f71697ec8724c11db9c46c1574b21e4
React-defaultsnativemodule: de00ba37b23205eca31cee5a6a8756a48f0a38f9
React-domnativemodule: b8dd2af2fcd232da5ea679e439a95a827a852a21
- React-Fabric: a63a42788096362a4840d827dab426c17a15a644
- React-FabricComponents: 8dd7bea45626350fe7bd052ea7c9fd657a1977cc
- React-FabricImage: de4c10c3319b916ca4d0b6a6fd53c3ecb4da0f83
+ React-Fabric: 31765f67f16bec0e85e89be0a544da0d91b8fdc2
+ React-FabricComponents: ad41201e8e20a4f57d9f62ded0506ebc67e89cff
+ React-FabricImage: 77b5157315924901911fdaa586df5b5fbf754366
React-featureflags: c6de2182d6065b3239245820347947e2c1309feb
React-featureflagsnativemodule: 47c7a5814a027eb262c2709852d1b1a597599cca
- React-graphics: bf16f4f74e3bc342b58b71797674a4eafda467a9
- React-hermes: a942bebef5e9fcc31f51c6fb814e96c260a2a20d
+ React-graphics: f9371eec432c7cf095238eaca8d2d3deaa95989f
+ React-hermes: 1ee5a6b835bc939b7aba500cb716a1095b3e0959
React-idlecallbacksnativemodule: d53c466886963cc325172bc9100b21e8511bce97
React-ImageManager: 4e2d837df0bf6b4cc42238122dc4577545befed5
React-jserrorhandler: 41f26e9e5559bda0549f27410d685a8f21bdbb94
- React-jsi: b2de88284fc2cc69466a34d8b794160216d3bd2c
- React-jsiexecutor: e947af1c9e42610affd9f4178cd4b649e8ac889b
+ React-jsi: 0810ff01e7f73a1cfba83d176e21c29302f6f3aa
+ React-jsiexecutor: 70f6865d195eba534f39ac9504a9aa178e5389f7
React-jsinspector: 7efabcd0a393fed8e3bd4b9c8c461c1b4d5d2b90
React-jsinspectortracing: 8cddf7d93b8aa43a14b5d92e8a83de083a1158e6
React-jsitracing: 1d636f7da6f2d4f9c2d3d7e9d50f469eb2356ddb
@@ -2548,7 +2548,7 @@ SPEC CHECKSUMS:
react-native-document-picker: 4f90a074d1eb269e32d5563c53b70d469180cece
react-native-fast-crypto: b30594570dab23aca7e74e206b2c03e28a006216
react-native-get-random-values: 384787fd76976f5aec9465aff6fa9e9129af1e74
- react-native-google-cast: 0a82cf63114470403e41e04ffa2b13d6448b6112
+ react-native-google-cast: 18b9b2fc518caabfa65d309409e160b3fc6d1733
react-native-image-picker: f104798044ef2c9211c42a48025d0693b5f34981
react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4
react-native-keyboard-controller: 7c1271a9fe703b7ee588b75d6c486eda79e5081b
@@ -2570,7 +2570,7 @@ SPEC CHECKSUMS:
React-RCTActionSheet: a078d5008632fed31b0024c420ee02e612b317d5
React-RCTAnimation: 82e31d191af4175e0c2df5bdac2c8569a5f3ab54
React-RCTAppDelegate: 183c991d0be0b05a4e1587bb2740cf954782a56b
- React-RCTBlob: c462b8b7de6ce44ddc56dd96eebe1da0a6e54c77
+ React-RCTBlob: 605c283c68fee9e095206fefbfe69d0d23df1a00
React-RCTFabric: 8f6eead6e48b1126ec325964040d175c9163e35f
React-RCTFBReactNativeSpec: bc4feee6ad7e6cce73d38000473fe8702a38e76a
React-RCTImage: 10fad63f1bb8adbd519c4c2ef6bec3c0d95fdd32
@@ -2580,7 +2580,7 @@ SPEC CHECKSUMS:
React-RCTText: d97cfb9c89b06de9530577dd43f178c47ea07853
React-RCTVibration: 2fcefee071a4f0d416e4368416bb073ea6893451
React-rendererconsistency: 259dede0b0b9b46bcc2fcdc94465a5fa01a66ef9
- React-rendererdebug: 3f7600015d8ce3a4c97149f3660fe30dba17c0fd
+ React-rendererdebug: 61d39f5756b6fad82d52c25e87ef71e35e4d4254
React-rncore: 93b049aef62762732c06413616b0f033d47b9a95
React-RuntimeApple: 85a29d8805ace62a2db360cc46e3100435b0dd2b
React-RuntimeCore: a81ea64fb5578c0367736c91fb85d6844950b7c2
@@ -2591,7 +2591,7 @@ SPEC CHECKSUMS:
React-utils: 21356bf3cdd7a337587165fea57563a077993864
ReactAppDependencyProvider: ad88c80e06f29900f2e6f9ccf1d4cb0bfc3e1bbc
ReactCodegen: 8e673528b686d334cfa785515d9c9b14bcb272d8
- ReactCommon: 7ea8ee50e489e9cc75922f19a06ea45c1b59b4bd
+ ReactCommon: 2a8b95bef15a281871b6bae12de1624f80e3a209
RNBootSplash: c2ae3f80f6c90979ee57977672f57e74981f85a5
RNCAsyncStorage: 23e56519cc41d3bade3c8d4479f7760cb1c11996
RNCClipboard: dfeb43751adff21e588657b5b6c888c72f3aa68e
diff --git a/packages/mobile/src/components/artist-coin-details-drawer/ArtistCoinDetailsDrawer.tsx b/packages/mobile/src/components/artist-coin-details-drawer/ArtistCoinDetailsDrawer.tsx
index 2aa4b2603de..f44a31d82d5 100644
--- a/packages/mobile/src/components/artist-coin-details-drawer/ArtistCoinDetailsDrawer.tsx
+++ b/packages/mobile/src/components/artist-coin-details-drawer/ArtistCoinDetailsDrawer.tsx
@@ -1,12 +1,26 @@
-import { useCallback } from 'react'
+import { useCallback, type ReactNode } from 'react'
-import { useArtistCoin, useCoinGeckoCoin } from '@audius/common/api'
+import { useArtistCoin, useCoinGeckoCoin, useUser } from '@audius/common/api'
import { coinDetailsMessages } from '@audius/common/messages'
import { useArtistCoinDetailsModal } from '@audius/common/store'
-import { formatCurrencyWithSubscript } from '@audius/common/utils'
+import {
+ formatCurrencyWithSubscript,
+ getTokenDecimalPlaces
+} from '@audius/common/utils'
+import { FixedDecimal, wAUDIO } from '@audius/fixed-decimal'
import Clipboard from '@react-native-clipboard/clipboard'
+import { ScrollView } from 'react-native'
-import { Flex, Text, Divider, Button, useTheme } from '@audius/harmony-native'
+import {
+ Flex,
+ Text,
+ Divider,
+ Button,
+ IconCopy,
+ PlainButton,
+ useTheme
+} from '@audius/harmony-native'
+import { TooltipInfoIcon } from 'app/components/buy-sell/TooltipInfoIcon'
import { TokenIcon } from 'app/components/core'
import Drawer from 'app/components/drawer/Drawer'
import { useToast } from 'app/hooks/useToast'
@@ -14,7 +28,79 @@ import { env } from 'app/services/env'
import { DrawerHeader } from '../drawer/DrawerHeader'
-const { artistCoinDetails } = coinDetailsMessages
+const { artistCoinDetails, overflowMenu } = coinDetailsMessages
+
+const LAUNCHPAD_COIN_DESCRIPTION = (handle: string, ticker: string) =>
+ `{${ticker}} is an artist coin created by {${handle}} on Audius. Learn more at https://audius.co/coin/{${ticker}}`
+
+const formatTokenAmount = (balance: number, coinDecimals: number) => {
+ const decimals = getTokenDecimalPlaces(balance)
+ const maxFractionDigits = Math.min(decimals, coinDecimals)
+ return new FixedDecimal(BigInt(balance), coinDecimals).toLocaleString(
+ 'en-US',
+ {
+ maximumFractionDigits: maxFractionDigits
+ }
+ )
+}
+
+const formatFeeNumber = (input: number) => {
+ const value = wAUDIO(BigInt(input))
+ const decimalPlaces = getTokenDecimalPlaces(Number(value.toString()))
+ return formatCurrencyWithSubscript(
+ Number(value.trunc(decimalPlaces).toString()),
+ 'en-US',
+ ''
+ )
+}
+
+const DetailRow = ({
+ label,
+ tooltip,
+ children
+}: {
+ label: string
+ tooltip?: string
+ children: ReactNode
+}) => (
+
+
+
+ {label}
+
+ {tooltip ? : null}
+
+ {children}
+
+)
+
+const StatRow = ({ left, right }: { left: ReactNode; right: ReactNode }) => (
+
+ {left}
+ {right}
+
+)
+
+const CopyableAddress = ({
+ address,
+ onCopy
+}: {
+ address: string
+ onCopy: () => void
+}) => (
+
+
+ {address}
+
+
+
+)
export const ArtistCoinDetailsDrawer = () => {
const { spacing } = useTheme()
@@ -22,6 +108,7 @@ export const ArtistCoinDetailsDrawer = () => {
const { isOpen, onClose, data: modalData } = useArtistCoinDetailsModal()
const mint = modalData?.mint
const { data: artistCoin } = useArtistCoin(mint)
+ const { data: artist } = useUser(artistCoin?.ownerId)
const isAudio = mint === env.WAUDIO_MINT_ADDRESS
const { data: coingeckoResponse } = useCoinGeckoCoin(
{ coinId: 'audius' },
@@ -35,160 +122,258 @@ export const ArtistCoinDetailsDrawer = () => {
}
}, [artistCoin?.mint, toast])
+ const handleCopyRewardsPoolAddress = useCallback(() => {
+ if (artistCoin?.rewardPool?.address) {
+ Clipboard.setString(artistCoin.rewardPool.address)
+ toast({ content: artistCoinDetails.copied, type: 'info' })
+ }
+ }, [artistCoin?.rewardPool?.address, toast])
+
if (!artistCoin?.mint) {
return null
}
- const renderHeader = () => {
- return (
-
-
-
-
- )
- }
+ const decimals = artistCoin.decimals ?? 9
+ const hasGraduated = artistCoin.dynamicBondingCurve?.isMigrated ?? false
+ const locker = artistCoin.artistLocker
+ const showLockerStats = !isAudio && hasGraduated && !!locker
+ const ticker = artistCoin.ticker ? `$${artistCoin.ticker}` : ''
+
+ // Market data
+ const totalSupply = isAudio
+ ? coingeckoResponse?.market_data?.total_supply
+ : artistCoin.totalSupply
+ const marketCap = isAudio
+ ? coingeckoResponse?.market_data?.market_cap?.usd
+ : artistCoin.displayMarketCap
+ const price = isAudio
+ ? coingeckoResponse?.market_data?.current_price?.usd
+ : artistCoin.displayPrice
+ const liquidity = isAudio
+ ? coingeckoResponse?.market_data?.total_volume?.usd
+ : artistCoin.liquidity
+
+ const formattedArtistEarnings = artistCoin.artistFees?.totalFees
+ ? formatFeeNumber(Math.trunc(artistCoin.artistFees.totalFees))
+ : null
+
+ const rewardsPoolBalance =
+ artistCoin.rewardPool?.balance != null
+ ? formatTokenAmount(artistCoin.rewardPool.balance, decimals)
+ : null
+
+ const renderHeader = () => (
+
+
+
+
+ )
return (
-
-
- {/* Token Info Section */}
-
+
+
+
+ {/* Token Info with avatar */}
- {/* Token Icon */}
-
+
- {artistCoin?.name}
+ {artistCoin.name}
- ${artistCoin?.ticker}
+ {ticker}
-
-
-
- {/* Coin Address */}
- {artistCoin?.mint ? (
-
-
- {artistCoinDetails.coinAddress}
-
-
- {artistCoin.mint}
-
-
- ) : null}
-
- {/* On-Chain Description */}
- {artistCoin?.description ? (
-
-
- {artistCoinDetails.onChainDescription}
-
-
- {artistCoin.description}
-
-
- ) : null}
-
- {/* Token Details */}
-
- {(
- isAudio
- ? coingeckoResponse?.market_data?.total_supply
- : artistCoin?.totalSupply
- ) ? (
-
-
- {artistCoinDetails.totalSupply}
-
-
- {isAudio
- ? coingeckoResponse?.market_data?.total_supply?.toLocaleString()
- : artistCoin?.totalSupply?.toLocaleString()}
-
-
- ) : null}
- {(
- isAudio
- ? coingeckoResponse?.market_data?.market_cap?.usd
- : artistCoin?.marketCap
- ) ? (
-
-
- {artistCoinDetails.marketCap}
-
-
- $
- {isAudio
- ? coingeckoResponse?.market_data?.market_cap?.usd?.toLocaleString()
- : artistCoin?.marketCap?.toLocaleString()}
-
-
- ) : null}
+
- {(
- isAudio
- ? coingeckoResponse?.market_data?.current_price?.usd
- : artistCoin?.price
- ) ? (
-
-
- {artistCoinDetails.price}
-
+ {/* Coin Address */}
+
+
+
+
+ {/* On-Chain Description */}
+ {!isAudio && artist?.handle ? (
+
- {formatCurrencyWithSubscript(
- isAudio
- ? (coingeckoResponse?.market_data?.current_price?.usd ?? 0)
- : (artistCoin?.price ?? 0)
+ {LAUNCHPAD_COIN_DESCRIPTION(
+ artist.handle,
+ artistCoin.ticker ?? ''
)}
-
+
) : null}
- {(
- isAudio
- ? coingeckoResponse?.market_data?.total_volume?.usd
- : artistCoin?.liquidity
- ) ? (
-
-
- {artistCoinDetails.liquidity}
-
-
- $
- {isAudio
- ? coingeckoResponse?.market_data?.total_volume?.usd?.toLocaleString()
- : artistCoin?.liquidity?.toLocaleString()}
-
-
- ) : null}
+
- {artistCoin?.circulatingSupply ? (
-
-
- {artistCoinDetails.circulatingSupply}
-
-
- {artistCoin.circulatingSupply.toLocaleString()}
-
-
+ {/* Market Stats */}
+
+
+
+ {totalSupply.toLocaleString()}
+
+
+ ) : null
+ }
+ right={
+ marketCap != null ? (
+
+
+ ${marketCap.toLocaleString()}
+
+
+ ) : null
+ }
+ />
+
+
+ {formatCurrencyWithSubscript(price)}
+
+
+ ) : null
+ }
+ right={
+ liquidity != null ? (
+
+
+ ${liquidity.toLocaleString()}
+
+
+ ) : null
+ }
+ />
+
+
+ {/* Vesting / Locker Stats - Non-wAUDIO only */}
+ {!isAudio ? (
+ <>
+
+
+ {/* Unlock Schedule */}
+
+
+ {overflowMenu.vestingScheduleValue}
+
+
+
+ {/* Artist Earnings */}
+ {formattedArtistEarnings ? (
+
+
+ {formattedArtistEarnings} {overflowMenu.$audio}
+
+
+ ) : null}
+
+ {/* Locked / Unlocked */}
+ {showLockerStats && locker ? (
+ <>
+
+
+ {formatTokenAmount(locker.locked ?? 0, decimals)}{' '}
+ {ticker}
+
+
+
+
+ {formatTokenAmount(locker.unlocked ?? 0, decimals)}{' '}
+ {ticker}
+
+
+ >
+ ) : null}
+
+ {/* Reward Pool */}
+ {rewardsPoolBalance != null ? (
+
+
+ {rewardsPoolBalance} {ticker}
+
+
+ ) : null}
+
+ {/* Rewards Pool Address */}
+ {artistCoin.rewardPool?.address ? (
+
+
+
+ ) : null}
+
+ >
) : null}
-
- {/* Close Button */}
-
-
+ {/* Close Button */}
+
+
+
+
+
)
}
diff --git a/packages/mobile/src/components/user-link/UserLink.tsx b/packages/mobile/src/components/user-link/UserLink.tsx
index 18c3dcb7d6e..044a5a22e26 100644
--- a/packages/mobile/src/components/user-link/UserLink.tsx
+++ b/packages/mobile/src/components/user-link/UserLink.tsx
@@ -26,6 +26,7 @@ type UserLinkProps = Omit, 'to' | 'children'> & {
textLinkStyle?: StyleProp
disabled?: boolean
hideArtistCoinBadge?: boolean
+ mint?: string
}
export const UserLink = (props: UserLinkProps) => {
@@ -36,6 +37,7 @@ export const UserLink = (props: UserLinkProps) => {
textLinkStyle,
disabled,
hideArtistCoinBadge,
+ mint,
...other
} = props
const navigation = useNavigation()
@@ -90,6 +92,7 @@ export const UserLink = (props: UserLinkProps) => {
diff --git a/packages/mobile/src/screens/artist-coins-explore-screen/ArtistCoinExploreCard.tsx b/packages/mobile/src/screens/artist-coins-explore-screen/ArtistCoinExploreCard.tsx
new file mode 100644
index 00000000000..0191c1d34ff
--- /dev/null
+++ b/packages/mobile/src/screens/artist-coins-explore-screen/ArtistCoinExploreCard.tsx
@@ -0,0 +1,286 @@
+import { useEffect, useMemo, useState } from 'react'
+
+import type { Coin } from '@audius/common/adapters'
+import { useUser } from '@audius/common/api'
+import { walletMessages } from '@audius/common/messages'
+import type { ID } from '@audius/common/models'
+import { WidthSizes } from '@audius/common/models'
+import { formatCount } from '@audius/common/utils'
+import type { ImageSourcePropType } from 'react-native'
+import { Image, StyleSheet, TouchableOpacity, View } from 'react-native'
+
+import {
+ Divider,
+ Flex,
+ Paper,
+ Skeleton,
+ Text,
+ spacing as harmonySpacing
+} from '@audius/harmony-native'
+import { ProfilePicture, TokenIcon } from 'app/components/core'
+import { useCoverPhoto } from 'app/components/image/CoverPhoto'
+import { primitiveToImageSource } from 'app/components/image/primitiveToImageSource'
+import { UserLink } from 'app/components/user-link'
+import { useThemeColors } from 'app/utils/theme'
+
+const COVER_HEIGHT = 96
+const AVATAR_OVERLAP = -harmonySpacing.unit9
+
+type ArtistCoinExploreCardProps = {
+ coin: Coin
+ onPress: () => void
+}
+
+const resolveDisplaySource = (
+ bannerTrim: string,
+ ownerCoverSource: ImageSourcePropType | undefined
+): ImageSourcePropType | undefined => {
+ if (bannerTrim) {
+ return primitiveToImageSource(bannerTrim)
+ }
+ if (!ownerCoverSource) {
+ return undefined
+ }
+ if (typeof ownerCoverSource === 'number') {
+ return ownerCoverSource
+ }
+ if (typeof ownerCoverSource === 'object' && ownerCoverSource !== null) {
+ if (
+ 'uri' in ownerCoverSource &&
+ ownerCoverSource.uri != null &&
+ String(ownerCoverSource.uri).length > 0
+ ) {
+ return ownerCoverSource
+ }
+ return undefined
+ }
+ return ownerCoverSource
+}
+
+const sourceTrackKey = (
+ bannerTrim: string,
+ ownerCoverSource: ImageSourcePropType | undefined
+) => {
+ if (bannerTrim) {
+ return `b:${bannerTrim}`
+ }
+ if (typeof ownerCoverSource === 'number') {
+ return `n:${ownerCoverSource}`
+ }
+ if (
+ typeof ownerCoverSource === 'object' &&
+ ownerCoverSource !== null &&
+ 'uri' in ownerCoverSource &&
+ ownerCoverSource.uri != null
+ ) {
+ return `c:${String(ownerCoverSource.uri)}`
+ }
+ return ''
+}
+
+/**
+ * Isolated per `mint` via parent `key` so FlashList recycling does not mix
+ * cover hooks between rows. Shimmers until the banner or owner cover loads.
+ */
+const ArtistCoinExploreCardCover = ({
+ bannerImageUrl,
+ ownerId
+}: {
+ bannerImageUrl?: string
+ ownerId: ID
+}) => {
+ const { borderDefault, neutralLight8 } = useThemeColors()
+ const { source: ownerCoverSource } = useCoverPhoto({
+ userId: ownerId,
+ size: WidthSizes.SIZE_640
+ })
+ const { isPending: isUserPending } = useUser(ownerId)
+
+ const bannerTrim = bannerImageUrl?.trim() ?? ''
+ const displaySource = useMemo(
+ () => resolveDisplaySource(bannerTrim, ownerCoverSource),
+ [bannerTrim, ownerCoverSource]
+ )
+
+ const trackKey = useMemo(
+ () => sourceTrackKey(bannerTrim, ownerCoverSource),
+ [bannerTrim, ownerCoverSource]
+ )
+
+ const [imageReady, setImageReady] = useState(false)
+
+ useEffect(() => {
+ setImageReady(false)
+ }, [trackKey])
+
+ useEffect(() => {
+ if (typeof displaySource === 'number') {
+ setImageReady(true)
+ }
+ }, [displaySource])
+
+ useEffect(() => {
+ if (!trackKey && !isUserPending) {
+ setImageReady(true)
+ }
+ }, [trackKey, isUserPending])
+
+ const waitingOnRemoteImage =
+ displaySource != null && typeof displaySource !== 'number'
+
+ const showShimmer =
+ !imageReady && (waitingOnRemoteImage || (!bannerTrim && isUserPending))
+
+ return (
+
+ {showShimmer ? : null}
+ {displaySource ? (
+ {
+ setImageReady(true)
+ }}
+ onError={() => {
+ setImageReady(true)
+ }}
+ />
+ ) : null}
+
+ )
+}
+
+export const ArtistCoinExploreCard = ({
+ coin,
+ onPress
+}: ArtistCoinExploreCardProps) => {
+ const ownerId = coin.ownerId
+
+ const fanClubLabel = walletMessages.artistCoins.fanClubLabel
+ const membersLabel = walletMessages.artistCoins.members
+ const marketCapLabel = walletMessages.artistCoins.marketCap
+
+ const membersDisplay =
+ coin.holder != null && !Number.isNaN(coin.holder)
+ ? coin.holder.toLocaleString('en-US')
+ : '—'
+
+ const marketCapDisplay = `$${formatCount(coin.displayMarketCap ?? 0, 2)}`
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {fanClubLabel}
+
+
+
+
+
+
+
+
+
+
+ {walletMessages.poweredBy}
+
+
+ {coin.ticker ?? coin.name}
+
+
+
+
+
+
+
+
+
+ {membersLabel}
+
+
+ {membersDisplay}
+
+
+
+
+ {marketCapLabel}
+
+
+ {marketCapDisplay}
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/mobile/src/screens/artist-coins-explore-screen/ArtistCoinsExploreScreen.tsx b/packages/mobile/src/screens/artist-coins-explore-screen/ArtistCoinsExploreScreen.tsx
index f0e37c82382..0e393e2955b 100644
--- a/packages/mobile/src/screens/artist-coins-explore-screen/ArtistCoinsExploreScreen.tsx
+++ b/packages/mobile/src/screens/artist-coins-explore-screen/ArtistCoinsExploreScreen.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from 'react'
+import { useState, useEffect, useCallback, useMemo } from 'react'
import type { Coin } from '@audius/common/adapters'
import {
@@ -10,7 +10,18 @@ import { walletMessages } from '@audius/common/messages'
import { useRoute } from '@react-navigation/native'
import type { ListRenderItem } from '@shopify/flash-list'
import { FlashList } from '@shopify/flash-list'
-import { ImageBackground, TouchableOpacity } from 'react-native'
+import {
+ ImageBackground,
+ TouchableOpacity,
+ useWindowDimensions,
+ View
+} from 'react-native'
+import {
+ TabView,
+ TabBar,
+ type Route as TabRoute,
+ type SceneRendererProps
+} from 'react-native-tab-view'
import { useDebounce } from 'react-use'
import {
@@ -26,20 +37,23 @@ import {
useTheme
} from '@audius/harmony-native'
import imageSearchHeaderBackground from 'app/assets/images/imageCoinsBackgroundImage.webp'
+import { GradientText, Screen, TokenIcon } from 'app/components/core'
import { PlayBarChin } from 'app/components/core/PlayBarChin'
import { UserLink } from 'app/components/user-link'
import { useNavigation } from 'app/hooks/useNavigation'
import { useStatusBarStyle } from 'app/hooks/useStatusBarStyle'
import { env } from 'app/services/env'
+import { makeStyles } from 'app/styles'
+import { useThemeColors } from 'app/utils/theme'
-import {
- GradientText,
- TokenIcon,
- Screen,
- VirtualizedScrollView
-} from '../../components/core'
+import { ArtistCoinExploreCard } from './ArtistCoinExploreCard'
+
+const COIN_ROW_HEIGHT = 56
+/** FlashList: slightly conservative height avoids mis-recycling tall cards when scrolling. */
+const COIN_CARD_ESTIMATED_HEIGHT = 400
+const EXPLORE_PAGE_SIZE = 12
-const COIN_ROW_HEIGHT = 50 // Estimated height for FlashList optimization
+const COIN_ROW_MESSAGES = walletMessages.artistCoins
type CoinRowProps = {
coin: Coin
@@ -53,7 +67,6 @@ const CoinRow = ({ coin, onPress }: CoinRowProps) => {
-
@@ -69,6 +82,7 @@ const CoinRow = ({ coin, onPress }: CoinRowProps) => {
size='xs'
badgeSize='2xs'
hideArtistCoinBadge
+ mint={coin.mint}
/>
@@ -81,10 +95,10 @@ const NoCoinsContent = () => {
- {walletMessages.artistCoins.noCoins}
+ {COIN_ROW_MESSAGES.noCoins}
- {walletMessages.artistCoins.noCoinsDescription}
+ {COIN_ROW_MESSAGES.noCoinsDescription}
)
@@ -98,6 +112,8 @@ const Header = ({
setSearchValue: (value: string) => void
}) => {
const navigation = useNavigation()
+ const { focus, backgroundSurface2 } = useThemeColors()
+
return (
@@ -106,11 +122,19 @@ const Header = ({
color='staticWhite'
onPress={() => navigation.goBack()}
/>
-
+
({
+ tabBar: {
+ backgroundColor: palette.white,
+ height: spacing(10),
+ elevation: 0,
+ shadowOpacity: 0,
+ borderBottomWidth: 1,
+ borderBottomColor: palette.borderDefault
+ },
+ tabIndicator: {
+ backgroundColor: palette.focus,
+ borderBottomLeftRadius: 20,
+ borderBottomRightRadius: 20,
+ height: 3,
+ bottom: -3
+ }
+}))
+
+const tabRoutes: TabRoute[] = [
+ { key: 'cards', title: COIN_ROW_MESSAGES.cardView },
+ { key: 'leaderboard', title: COIN_ROW_MESSAGES.leaderboardView }
+]
+
export const ArtistCoinsExploreScreen = () => {
const { typography } = useTheme()
const route = useRoute()
const navigation = useNavigation()
+ const layout = useWindowDimensions()
+ const tabStyles = useTabStyles()
+ const { textIconSubdued, neutral, backgroundSurface2 } = useThemeColors()
+
const [searchValue, setSearchValue] = useState('')
const [debouncedSearchValue, setDebouncedSearchValue] = useState('')
+ const [tabIndex, setTabIndex] = useState(0)
const [sortMethod, setSortMethod] = useState(
GetCoinsSortMethodEnum.MarketCap
)
@@ -135,10 +187,8 @@ export const ArtistCoinsExploreScreen = () => {
GetCoinsSortDirectionEnum.Desc
)
- // Set status bar to light content for dark header
useStatusBarStyle('light-content')
- // Debounce search value to avoid excessive API calls
useDebounce(
() => {
setDebouncedSearchValue(searchValue)
@@ -155,14 +205,16 @@ export const ArtistCoinsExploreScreen = () => {
hasNextPage,
isFetchingNextPage
} = useArtistCoins({
+ pageSize: EXPLORE_PAGE_SIZE,
sortMethod,
sortDirection,
query: debouncedSearchValue
})
- // Filter out WAUDIO
- const allCoins =
- coins?.filter((coin) => coin.mint !== env.WAUDIO_MINT_ADDRESS) ?? []
+ const allCoins = useMemo(
+ () => coins?.filter((coin) => coin.mint !== env.WAUDIO_MINT_ADDRESS) ?? [],
+ [coins]
+ )
const handleCoinPress = useCallback(
(ticker?: string) => {
@@ -187,7 +239,10 @@ export const ArtistCoinsExploreScreen = () => {
}, [isFetchingNextPage, hasNextPage, fetchNextPage])
useEffect(() => {
- const routeParams = route.params as any
+ const routeParams = route.params as {
+ sortMethod?: GetCoinsSortMethodEnum
+ sortDirection?: GetCoinsSortDirectionEnum
+ }
if (routeParams?.sortMethod) {
setSortMethod(routeParams.sortMethod)
}
@@ -206,6 +261,16 @@ export const ArtistCoinsExploreScreen = () => {
[handleCoinPress]
)
+ const renderCoinCard: ListRenderItem = useCallback(
+ ({ item }) => (
+ handleCoinPress(item.ticker)}
+ />
+ ),
+ [handleCoinPress]
+ )
+
const keyExtractor = useCallback((coin: Coin) => coin.mint, [])
const renderFooter = useCallback(() => {
@@ -218,68 +283,174 @@ export const ArtistCoinsExploreScreen = () => {
}
return null
}, [isFetchingNextPage])
- return (
- (
-
- )}
- >
-
- (
+
+ ),
+ [tabStyles.tabBar, tabStyles.tabIndicator, neutral, textIconSubdued]
+ )
+
+ const tabCommonOptions = useMemo(
+ () => ({
+ label: ({
+ focused,
+ route: tabRoute
+ }: {
+ focused: boolean
+ route: TabRoute
+ }) => (
+
-
-
- {walletMessages.artistCoins.title}
-
-
+ {tabRoute.title}
+
+ )
+ }),
+ []
+ )
+
+ const renderScene = useCallback(
+ ({ route: sceneRoute }: SceneRendererProps & { route: TabRoute }) => {
+ if (sceneRoute.key === 'cards') {
+ return (
+
+ {isPending ? (
+
+
+ ) : shouldShowNoCoinsContent ? (
+
+ ) : (
+ }
+ contentContainerStyle={{
+ paddingHorizontal: 16,
+ paddingVertical: 16
+ }}
+ />
+ )}
+
+ )
+ }
+
+ return (
+
+
+
+
-
+
+ {COIN_ROW_MESSAGES.title}
+
+
+
+
+
+
-
-
-
- {isPending ? (
-
-
+
+ {isPending ? (
+
+
+
+ ) : shouldShowNoCoinsContent ? (
+
+ ) : (
+
+ )}
- ) : shouldShowNoCoinsContent ? (
-
- ) : (
-
- )}
-
-
+
+
+ )
+ },
+ [
+ allCoins,
+ backgroundSurface2,
+ handleLoadMore,
+ handleSortPress,
+ isPending,
+ keyExtractor,
+ renderCoinCard,
+ renderCoinRow,
+ renderFooter,
+ shouldShowNoCoinsContent,
+ typography.fontByWeight.bold,
+ typography.size.l
+ ]
+ )
+
+ return (
+ (
+
+ )}
+ >
+
+
+
)
diff --git a/packages/mobile/src/screens/coin-details-screen/CoinDetailsScreen.tsx b/packages/mobile/src/screens/coin-details-screen/CoinDetailsScreen.tsx
index e5a633dd904..bb1aa8ab8d1 100644
--- a/packages/mobile/src/screens/coin-details-screen/CoinDetailsScreen.tsx
+++ b/packages/mobile/src/screens/coin-details-screen/CoinDetailsScreen.tsx
@@ -1,26 +1,76 @@
+import { useState, useCallback, useMemo } from 'react'
+
import { useArtistCoinByTicker } from '@audius/common/api'
import { route } from '@audius/common/utils'
import { useRoute } from '@react-navigation/native'
+import { useWindowDimensions } from 'react-native'
+import {
+ TabView,
+ TabBar,
+ type Route as TabRoute,
+ type SceneRendererProps
+} from 'react-native-tab-view'
-import { Flex, IconButton, IconKebabHorizontal } from '@audius/harmony-native'
+import {
+ Flex,
+ IconButton,
+ IconKebabHorizontal,
+ Text
+} from '@audius/harmony-native'
import { Screen, ScreenContent, ScrollView } from 'app/components/core'
import { useDrawer } from 'app/hooks/useDrawer'
+import { makeStyles } from 'app/styles'
+import { useThemeColors } from 'app/utils/theme'
+
+import { CoinTab } from './components/CoinTab'
+import { FanClubTab } from './components/FanClubTab'
+
+const messages = {
+ fanClub: 'Fan Club',
+ coin: 'Coin'
+}
-import { BalanceCard } from './components/BalanceCard'
-import { CoinInfoCard } from './components/CoinInfoCard'
-import { CoinInsightsCard } from './components/CoinInsightsCard'
-import { CoinLeaderboardCard } from './components/CoinLeaderboardCard'
-import { ExclusiveTracksSection } from './components/ExclusiveTracksSection'
+const useStyles = makeStyles(({ palette, spacing }) => ({
+ tabBar: {
+ backgroundColor: palette.white,
+ height: spacing(10),
+ elevation: 0,
+ shadowOpacity: 0,
+ borderBottomWidth: 1,
+ borderBottomColor: palette.borderDefault
+ },
+ tabIndicator: {
+ backgroundColor: palette.focus,
+ borderBottomLeftRadius: 20,
+ borderBottomRightRadius: 20,
+ height: 3,
+ bottom: -3
+ }
+}))
+
+const tabRoutes: TabRoute[] = [
+ { key: 'fanClub', title: messages.fanClub },
+ { key: 'coin', title: messages.coin }
+]
export const CoinDetailsScreen = () => {
const { ticker } = useRoute().params as { ticker: string }
const { data: coin } = useArtistCoinByTicker({ ticker })
const { onOpen } = useDrawer('CoinInsightsOverflowMenu')
const mint = coin?.mint ?? ''
+ const layout = useWindowDimensions()
+ const styles = useStyles()
+ const { textIconSubdued, neutral } = useThemeColors()
- const handleOpenOverflowMenu = () => {
+ const [tabIndex, setTabIndex] = useState(0)
+
+ const handleOpenOverflowMenu = useCallback(() => {
onOpen({ mint })
- }
+ }, [onOpen, mint])
+
+ const handleSwitchToCoinTab = useCallback(() => {
+ setTabIndex(1)
+ }, [])
const topbarRight = (
{
/>
)
+ const coinName = coin?.name ?? (ticker ? `$${ticker}` : 'Coin Details')
+
+ const renderScene = useCallback(
+ ({ route: tabRoute }: SceneRendererProps & { route: TabRoute }) => {
+ switch (tabRoute.key) {
+ case 'fanClub':
+ return (
+
+
+
+
+
+ )
+ case 'coin':
+ return (
+
+
+
+
+
+ )
+ default:
+ return null
+ }
+ },
+ [mint, handleSwitchToCoinTab]
+ )
+
+ const tabCommonOptions = useMemo(
+ () => ({
+ label: ({
+ focused,
+ route: tabRoute
+ }: {
+ focused: boolean
+ route: TabRoute
+ }) => (
+
+ {tabRoute.title}
+
+ )
+ }),
+ []
+ )
+
+ const renderTabBar = useCallback(
+ (props: any) => (
+
+ ),
+ [styles.tabBar, styles.tabIndicator, neutral, textIconSubdued]
+ )
+
return (
-
-
-
-
-
-
-
-
-
+
)
diff --git a/packages/mobile/src/screens/coin-details-screen/components/CoinInsightsCard.tsx b/packages/mobile/src/screens/coin-details-screen/components/CoinInsightsCard.tsx
index c61acaeb6c8..f65491db62a 100644
--- a/packages/mobile/src/screens/coin-details-screen/components/CoinInsightsCard.tsx
+++ b/packages/mobile/src/screens/coin-details-screen/components/CoinInsightsCard.tsx
@@ -1,27 +1,35 @@
+import { useCallback } from 'react'
+
import type { Coin } from '@audius/common/adapters'
import { useArtistCoin, useCoinGeckoCoin } from '@audius/common/api'
import { coinDetailsMessages } from '@audius/common/messages'
import {
createAudioCoinMetrics,
createCoinMetrics,
+ shortenSPLAddress,
type MetricData
} from '@audius/common/utils'
+import Clipboard from '@react-native-clipboard/clipboard'
import {
Flex,
IconCaretDown,
IconCaretUp,
+ IconCopy,
Paper,
+ PlainButton,
Text,
spacing
} from '@audius/harmony-native'
import { TooltipInfoIcon } from 'app/components/buy-sell/TooltipInfoIcon'
+import { useToast } from 'app/hooks/useToast'
import { env } from 'app/services/env'
import { isIos } from 'app/utils/os'
import { GraduationProgressBar } from './GraduationProgressBar'
const messages = coinDetailsMessages.coinInsights
+const overflowMessages = coinDetailsMessages.overflowMenu
const GraduatedPill = () => {
return (
@@ -152,6 +160,34 @@ const MetricRow = ({ metric, coin }: { metric: MetricData; coin?: Coin }) => {
)
}
+const InsightsCopyMintRow = ({ mint }: { mint: string }) => {
+ const { toast } = useToast()
+
+ const handleCopyAddress = useCallback(() => {
+ Clipboard.setString(mint)
+ toast({ content: overflowMessages.copiedToClipboard, type: 'info' })
+ }, [mint, toast])
+
+ return (
+
+
+ {overflowMessages.copyCoinAddress}
+
+
+ {shortenSPLAddress(mint)}
+
+
+ )
+}
+
export const CoinInsightsCard = ({ mint }: { mint: string }) => {
const isAudio = mint === env.WAUDIO_MINT_ADDRESS
const {
@@ -198,7 +234,7 @@ export const CoinInsightsCard = ({ mint }: { mint: string }) => {
- {isError || !coin ? (
+ {isError ? (
{messages.unableToLoad}
@@ -209,6 +245,7 @@ export const CoinInsightsCard = ({ mint }: { mint: string }) => {
))
)}
+ {mint ? : null}
)
}
diff --git a/packages/mobile/src/screens/coin-details-screen/components/CoinLeaderboardCard.tsx b/packages/mobile/src/screens/coin-details-screen/components/CoinLeaderboardCard.tsx
index 2888e7d5b7d..bdcef34c58e 100644
--- a/packages/mobile/src/screens/coin-details-screen/components/CoinLeaderboardCard.tsx
+++ b/packages/mobile/src/screens/coin-details-screen/components/CoinLeaderboardCard.tsx
@@ -38,7 +38,14 @@ export const CoinLeaderboardCard = ({ mint }: { mint: string }) => {
})
}, [mint, navigation])
- if (!mint || !users?.length) return null
+ if (!mint) {
+ return null
+ }
+
+ const memberCount = leaderboardUsers?.length ?? 0
+ if (!isLeaderboardPending && memberCount === 0) {
+ return null
+ }
return (
{
w='100%'
>
{isPending ? (
-
+
) : (
diff --git a/packages/mobile/src/screens/coin-details-screen/components/CoinTab.tsx b/packages/mobile/src/screens/coin-details-screen/components/CoinTab.tsx
new file mode 100644
index 00000000000..46f3942538e
--- /dev/null
+++ b/packages/mobile/src/screens/coin-details-screen/components/CoinTab.tsx
@@ -0,0 +1,180 @@
+import type { ReactNode } from 'react'
+
+import type { Coin } from '@audius/common/adapters'
+import { useArtistCoin, useCurrentUserId } from '@audius/common/api'
+import { coinDetailsMessages } from '@audius/common/messages'
+import {
+ getTokenDecimalPlaces,
+ formatCurrencyWithSubscript
+} from '@audius/common/utils'
+import { FixedDecimal, wAUDIO } from '@audius/fixed-decimal'
+
+import { Divider, Flex, Paper, Text } from '@audius/harmony-native'
+import { TooltipInfoIcon } from 'app/components/buy-sell/TooltipInfoIcon'
+import { env } from 'app/services/env'
+
+import { BalanceCard } from './BalanceCard'
+import { CoinInsightsCard } from './CoinInsightsCard'
+
+const overflowMessages = coinDetailsMessages.overflowMenu
+
+type CoinTabProps = {
+ mint: string
+}
+
+const formatTokenAmount = (balance: number, coinDecimals: number) => {
+ const decimals = getTokenDecimalPlaces(balance)
+ const maxFractionDigits = Math.min(decimals, coinDecimals)
+ return new FixedDecimal(BigInt(balance), coinDecimals).toLocaleString(
+ 'en-US',
+ {
+ maximumFractionDigits: maxFractionDigits
+ }
+ )
+}
+
+const formatFeeNumber = (input: number) => {
+ const value = wAUDIO(BigInt(input))
+ const decimalPlaces = getTokenDecimalPlaces(Number(value.toString()))
+ return formatCurrencyWithSubscript(
+ Number(value.trunc(decimalPlaces).toString()),
+ 'en-US',
+ ''
+ )
+}
+
+const CoinDetailRow = ({
+ label,
+ tooltip,
+ children
+}: {
+ label: string
+ tooltip?: string
+ children: ReactNode
+}) => (
+
+
+
+ {label}
+
+ {tooltip ? : null}
+
+ {children}
+
+)
+
+const CoinDetailsSection = ({ coin }: { coin: Coin }) => {
+ const isAudio = coin.mint === env.WAUDIO_MINT_ADDRESS
+ if (isAudio) return null
+
+ const hasGraduated = coin.dynamicBondingCurve?.isMigrated ?? false
+ const locker = coin.artistLocker
+ const showLockerStats = hasGraduated && !!locker
+ const decimals = coin.decimals ?? 9
+ const ticker = coin.ticker ? `$${coin.ticker}` : ''
+
+ const formattedArtistEarnings = coin.artistFees?.totalFees
+ ? formatFeeNumber(Math.trunc(coin.artistFees.totalFees))
+ : null
+
+ const rewardsPoolBalance =
+ coin.rewardPool?.balance != null
+ ? formatTokenAmount(coin.rewardPool.balance, decimals)
+ : null
+
+ return (
+
+
+
+ Coin Details
+
+
+
+
+
+ {/* Artist Earnings */}
+ {formattedArtistEarnings ? (
+
+
+ {formattedArtistEarnings} {overflowMessages.$audio}
+
+
+ ) : null}
+
+ {/* Unlock Schedule */}
+
+
+ {overflowMessages.vestingScheduleValue}
+
+
+
+ {/* Locked / Unlocked */}
+ {showLockerStats && locker ? (
+ <>
+
+
+ {formatTokenAmount(locker.locked ?? 0, decimals)} {ticker}
+
+
+
+
+
+ {formatTokenAmount(locker.unlocked ?? 0, decimals)} {ticker}
+
+
+ >
+ ) : null}
+
+ {/* Reward Pool */}
+ {rewardsPoolBalance != null ? (
+
+
+ {rewardsPoolBalance} {ticker}
+
+
+ ) : null}
+
+
+ )
+}
+
+export const CoinTab = ({ mint }: CoinTabProps) => {
+ const { data: coin } = useArtistCoin(mint)
+ const { data: currentUserId } = useCurrentUserId()
+ const isOwner = currentUserId === coin?.ownerId
+
+ return (
+
+ {/* Balance section */}
+
+
+ {/* Insights section (includes copy mint row) */}
+
+
+ {/* Coin Details - Owner only */}
+ {isOwner && coin ? : null}
+
+ )
+}
diff --git a/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx b/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx
new file mode 100644
index 00000000000..f8104fff461
--- /dev/null
+++ b/packages/mobile/src/screens/coin-details-screen/components/FanClubTab.tsx
@@ -0,0 +1,390 @@
+import { useCallback, useMemo } from 'react'
+
+import {
+ useArtistCoin,
+ useCoinBalance,
+ useCurrentUserId,
+ useExclusiveTracks,
+ useExclusiveTracksCount
+} from '@audius/common/api'
+import { useBuySellInitialTab, useIsManagedAccount } from '@audius/common/hooks'
+import { coinDetailsMessages, walletMessages } from '@audius/common/messages'
+import { WidthSizes } from '@audius/common/models'
+import {
+ exclusiveTracksPageLineupActions as exclusiveTracksActions,
+ receiveTokensModalActions
+} from '@audius/common/store'
+import { Image, StyleSheet, TouchableOpacity, View } from 'react-native'
+import { useDispatch } from 'react-redux'
+
+import {
+ Button,
+ Flex,
+ IconCaretRight,
+ IconCloudUpload,
+ LoadingSpinner,
+ Paper,
+ Text,
+ spacing as harmonySpacing
+} from '@audius/harmony-native'
+import { ProfilePicture, TokenIcon } from 'app/components/core'
+import { useCoverPhoto } from 'app/components/image/CoverPhoto'
+import { primitiveToImageSource } from 'app/components/image/primitiveToImageSource'
+import { TanQueryLineup } from 'app/components/lineup/TanQueryLineup'
+import { UserLink } from 'app/components/user-link'
+import { useNavigation } from 'app/hooks/useNavigation'
+import { useThemeColors } from 'app/utils/theme'
+
+import { CoinLeaderboardCard } from './CoinLeaderboardCard'
+
+const FAN_CLUB_COVER_HEIGHT = 96
+const FAN_CLUB_AVATAR_OVERLAP = -harmonySpacing.unit9
+
+const messages = {
+ uploadExclusiveTrack: coinDetailsMessages.coinInfo.uploadExclusiveTrack,
+ becomeAMember: coinDetailsMessages.balance.becomeAMember,
+ hintDescription: coinDetailsMessages.balance.hintDescription,
+ fanClubFeed: 'Fan Club Feed'
+}
+
+const MAX_PREVIEW_TRACKS = 3
+
+const itemStyles = {
+ paddingHorizontal: 0
+}
+
+type FanClubTabProps = {
+ mint: string
+ onSwitchToCoinTab: () => void
+}
+
+const BecomeAMemberCard = ({
+ ticker,
+ mint,
+ coinTicker
+}: {
+ ticker: string
+ mint: string
+ coinTicker?: string
+}) => {
+ const dispatch = useDispatch()
+ const navigation = useNavigation()
+ const isManagerMode = useIsManagedAccount()
+ const initialTab = useBuySellInitialTab()
+
+ const handleBuy = useCallback(() => {
+ navigation.navigate('BuySell', {
+ initialTab,
+ coinTicker
+ })
+ }, [navigation, initialTab, coinTicker])
+
+ const handleReceive = useCallback(() => {
+ dispatch(receiveTokensModalActions.open({ mint, isOpen: true }))
+ }, [dispatch, mint])
+
+ return (
+
+
+
+ {messages.becomeAMember}
+
+ {messages.hintDescription(ticker)}
+
+
+
+
+
+
+ )
+}
+
+type FanClubHeroTileProps = {
+ mint: string
+ onPoweredByPress: () => void
+}
+
+const FanClubHeroTile = ({ mint, onPoweredByPress }: FanClubHeroTileProps) => {
+ const { borderDefault } = useThemeColors()
+ const { data: coin, isLoading } = useArtistCoin(mint)
+ const ownerId = coin?.ownerId
+
+ const { source: coverPhotoSource } = useCoverPhoto({
+ userId: ownerId,
+ size: WidthSizes.SIZE_640
+ })
+
+ const bannerImageSource = useMemo(() => {
+ if (coin?.bannerImageUrl) {
+ return primitiveToImageSource(coin.bannerImageUrl)
+ }
+ return coverPhotoSource
+ }, [coin?.bannerImageUrl, coverPhotoSource])
+
+ if (isLoading || !coin || !ownerId) {
+ return null
+ }
+
+ const fanClubLabel = walletMessages.artistCoins.fanClubLabel
+
+ return (
+
+
+ {bannerImageSource ? (
+
+ ) : null}
+
+
+
+
+
+
+
+ {fanClubLabel}
+
+
+
+
+
+
+
+
+
+
+ {walletMessages.poweredBy}
+
+
+ {coin.ticker ?? coin.name}
+
+
+
+
+
+
+ {coin.description ? (
+
+ {coin.description}
+
+ ) : null}
+
+
+ )
+}
+
+const FanClubFeed = ({
+ mint,
+ forMemberView
+}: {
+ mint: string
+ forMemberView?: boolean
+}) => {
+ const { data: coin } = useArtistCoin(mint)
+ const ownerId = coin?.ownerId
+
+ const { data, lineup, pageSize, isFetching, loadNextPage, isPending } =
+ useExclusiveTracks({
+ userId: ownerId,
+ pageSize: MAX_PREVIEW_TRACKS
+ })
+
+ const { data: totalCount = 0, isPending: isCountPending } =
+ useExclusiveTracksCount({
+ userId: ownerId
+ })
+
+ if (!ownerId) {
+ return null
+ }
+
+ if (forMemberView) {
+ if (isCountPending) {
+ return (
+
+
+
+ {messages.fanClubFeed}
+
+
+
+
+
+
+ )
+ }
+ if (totalCount === 0) {
+ return null
+ }
+ } else if (totalCount === 0) {
+ return null
+ }
+
+ return (
+
+
+
+ {messages.fanClubFeed}
+
+ {totalCount > 0 ? (
+
+ ({totalCount})
+
+ ) : null}
+
+
+
+ )
+}
+
+export const FanClubTab = ({ mint, onSwitchToCoinTab }: FanClubTabProps) => {
+ const { data: coin } = useArtistCoin(mint)
+ const { data: currentUserId, isPending: isCurrentUserPending } =
+ useCurrentUserId()
+ const { data: tokenBalance, isPending: isBalancePending } = useCoinBalance({
+ mint
+ })
+ const navigation = useNavigation()
+
+ const isOwner =
+ !isCurrentUserPending &&
+ currentUserId != null &&
+ coin != null &&
+ currentUserId === coin.ownerId
+
+ const hasBalance =
+ !isBalancePending &&
+ !!(tokenBalance?.balance && Number(tokenBalance.balance.toString()) > 0)
+
+ const membershipKnown =
+ coin != null && !isCurrentUserPending && (isOwner || !isBalancePending)
+
+ const isMemberOrOwner = isOwner || hasBalance
+
+ const handleUploadExclusive = useCallback(() => {
+ navigation.navigate('Upload', {})
+ }, [navigation])
+
+ if (!coin) return null
+
+ const ticker = coin.ticker ?? ''
+
+ return (
+
+
+
+ {/* Upload Exclusive Track - Artist only */}
+ {isOwner ? (
+
+ ) : null}
+
+ {/* Membership CTA / leaderboard: wait until account + balance are known to avoid CLS */}
+ {!membershipKnown ? (
+
+
+
+ ) : !isMemberOrOwner ? (
+
+ ) : null}
+
+ {membershipKnown && isMemberOrOwner ? (
+
+ ) : null}
+
+ {membershipKnown ? (
+
+ ) : null}
+
+ )
+}