diff --git a/package-lock.json b/package-lock.json index 382d61ba269..42065d2330f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5746,6 +5746,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -7076,6 +7077,27 @@ "react": ">=16.8.0" } }, + "node_modules/@dr.pogodin/react-native-fs": { + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@dr.pogodin/react-native-fs/-/react-native-fs-2.32.1.tgz", + "integrity": "sha512-vNPgyPpw4OGaooIay8RUx0qUaFExas0ce6kmud1oTKjqeohEqwzuFZBa7gJ1bmF2cEkplp8sUzw2BVv4NDPrVA==", + "license": "MIT", + "workspaces": [ + "example" + ], + "dependencies": { + "buffer": "^6.0.3", + "http-status-codes": "^2.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/birdofpreyru" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@dual-bundle/import-meta-resolve": { "version": "4.1.0", "dev": true, @@ -12681,6 +12703,635 @@ "node": ">=14" } }, + "node_modules/@firebase/ai": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.13.0.tgz", + "integrity": "sha512-nJJDQKqjAcbkZdZGT/5WTVLrGZ+pYhWbwKC90nNzmvtoRTtnOJaNS34fhKSHQeB9SALgD2kxuWT5I4AkytdZ/Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.4", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.22", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.22.tgz", + "integrity": "sha512-8BSaq/QRGU1+xyi8L2PTLTJU7MH9aMA72RQdIxrbhWFauOZY9OXo8f2YDN/972xA8d588tlnNVEQ2Mo69pT9Ow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.28", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.28.tgz", + "integrity": "sha512-lIAlqUUbBu93FJMlQfslryQtBwwzdzvp23ePC6FNgymXk6Ook5v4Uvc0vdutvoIeqmyA3LfP0ZeRFK8+11kOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.22", + "@firebase/analytics-types": "0.8.4", + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.4.tgz", + "integrity": "sha512-zQ+XTgkwH6CY/eUSHJRP7e4LxM30RCxlCmob5sy2axs25GE3Ny0XdgpDscMTHHQIGqWkxPXad4w2Mw9sCgT8zQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.13.tgz", + "integrity": "sha512-H89Jeyp31+EZk9GPu6vaeL9mEmoXgM3nASB7UPBYYS/lqAks21mO1BU1dF8NbsVTL6tgGZkGUtiGJgxtDiwHkw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.4.tgz", + "integrity": "sha512-G8EsbVJV9gSfoibx0dNoNOUrvr+PkL7J//+W/BST/oUassimkZeq9bjj3bKkB0pn4og5GMQ9qs7FefwP00kkgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.4.tgz", + "integrity": "sha512-9iP0MvmaVagulNXmrca96U3tqNAI3j98wsC1z7rj62nnOTajlrHM//jjB9VoHqRw6/islMskp6RsKnM7vhLDqA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.4", + "@firebase/app-check-types": "0.5.4", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.4.tgz", + "integrity": "sha512-zz3i6e13B8BfWiLy8MABtTh8aGIACgKbf9UVnyHcWs+yQzJXgQcl8A46b0zfaiJHdQ+niF0ouAfcpuf+3LMPQg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.4.tgz", + "integrity": "sha512-xV7JsIyzVr15aA7f3Pi0rB9gdBuVubs89FGA8VkRYA4g0l78poADgdfrScgf7NndSg9mm7cR7PJyY0+t22KaGw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.13.tgz", + "integrity": "sha512-pn3FvXwUR34kWPccDQfCKsNZcM2wD1OS+J1jeEgzM1ZNXoxR2NaF6e5DjDuRrnTwR6LN2XQQt0IqE6yKmgpCQg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.13", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.5.tgz", + "integrity": "sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.1" + } + }, + "node_modules/@firebase/app/node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/@firebase/auth": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.13.2.tgz", + "integrity": "sha512-B4w0iS7MxRg28oIh2fJFTE6cM0lYdBrW19eHpc42jqEcloUjlYyVrpPqZvqA4+v9KFEVSKEs2SfWyta7hbzkJQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.7.tgz", + "integrity": "sha512-XgKnOgY1Siq7gylAmLkYtHAlRxNeWEAspH+nO3gJZJnfHqoTHbr9UjJ3nHNFALYXV5CfpQlyPROyB2ztySBHBQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.13.2", + "@firebase/auth-types": "0.13.1", + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.5.tgz", + "integrity": "sha512-1Li/YuBDBAXcKv7BzY4U28gontUmAaw53sYiqbaVOMCFb2lFKK/c3CGMUWqtwe7+TXrl3poWnTCL5umYBg85Eg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.1.tgz", + "integrity": "sha512-0c1Mnid0uMDfGJHeUS4zfvBa4/CedJXotGy/n/NZJnBjwiJawt0ZYU+wH2VAVLiRCEfG2ncCkAX3yd1/2nrB7g==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.3.tgz", + "integrity": "sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.7.1.tgz", + "integrity": "sha512-2LbUU8mmSA63HknxQMmWHjpzuNLBKflvVwQc2tpoVKg0biWleNEJX031ELks0vzFs+dDjOUkCJR72RP6mQHFOg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.5", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.3.tgz", + "integrity": "sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.4", + "@firebase/auth-interop-types": "0.2.5", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.4.tgz", + "integrity": "sha512-3pK35F1MAgmqFJQlf2nhQl44vtAXQO1uaCaQOEUI9kCRtLFqi7N+QRKR7lFZPg+xIZIyubgxQaxY69YgfZRZWg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/database": "1.1.3", + "@firebase/database-types": "1.0.20", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.20.tgz", + "integrity": "sha512-kegbOk/w8iU64pr0q6k2ItyNGjnQBMHFhwS7ohdWI4W+pc0/zhhdGXTdFj6X1oxItRjPoYOsSQmERgBkn/ihxw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.5", + "@firebase/util": "1.15.1" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.15.0.tgz", + "integrity": "sha512-Fj9osqYkz2Rqr7kW3/A8BRd8CyJ7yA5K8YjhihRdyJWbL+FsELVcR6DpoCplrp1IyU+xeGgTubo1UOySXpY+EA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "@firebase/webchannel-wrapper": "1.0.6", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.10.tgz", + "integrity": "sha512-yMP3FADDjikdrQv4YmvL4EkIny6Hw+N+a2O5T40rlHiniyMpRPxgYkKiFOvMZnsqKLqBVnKqCAElC0pa/IZtdw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/firestore": "4.15.0", + "@firebase/firestore-types": "3.0.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.4.tgz", + "integrity": "sha512-jGn+JSS4X9zZsrfu7Yw66v5YRdOLD1oyQh4USR0xWl4CUqV/DA6bNIXRPpxH/cUl3iVTNiP6MN7g+EL42A4qfA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.5.tgz", + "integrity": "sha512-bWCx713f4kE/uFV7gdFOLBS7lDoiZj48MRkbAqe35gkXcCeWF4QjRNO07Jhmve7EJIoQOBczL29y2r8VRuN1kw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.4", + "@firebase/auth-interop-types": "0.2.5", + "@firebase/component": "0.7.3", + "@firebase/messaging-interop-types": "0.2.5", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.5.tgz", + "integrity": "sha512-10qlUXGY25G5/1g9UihqksPp2po+ZqSE7LEizsrdUP7vrTmkysXxGSZCDyojSEp6mQe/ecRDdDDI+z4XRdb4wQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/functions": "0.13.5", + "@firebase/functions-types": "0.6.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.4.tgz", + "integrity": "sha512-zV6kgqtduR4rUAdC/ilS7kmb93XD7bEZoJDlVBZqlOw2uGGGCNBQBuleww2rr0Ulr3L9o2TDjumEt68/l1f9DQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.22", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.22.tgz", + "integrity": "sha512-ef6nn3GGQTdReCfotRMG77PJZu8CqEbiK5pEoBnM0gTu/Z9v0i/az2p3HABsa/1beQmmyh1OsOjf7P5+pgwdZw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.22.tgz", + "integrity": "sha512-C/zpAuTP5S9OgKSPvXRupw3hoY/JZSlA1wFjD/Sb7LIQE0FNbcMdO8Y4KXVEkjVzma/DDDDIAzxEXqKMAzc88w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/installations-types": "0.5.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.4.tgz", + "integrity": "sha512-U2eFapdHwjb43Vx9o+Pmj4dFfvcHEK1IirEFLqMtWrTHvmdrS3gBpBD1kmJk/9HjsOtoHZxJ2Paoe79e+L1ZPg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/@firebase/logger": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.1.tgz", + "integrity": "sha512-vZKLsqE1ABOy8OjQiE7cUTFn4gvaqlk88yp8N94Pk/sDpq61YqZGqmVFZTvOyflTwuYFcWirBdYGoJgbDaXKYQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.13.0.tgz", + "integrity": "sha512-GZoo0uGRvEbszo83xcgbjJp4FpkmBEr4l8Z4hi8gl+P1Spn/MTK3HapanMzSX4yUHuTEiF5hasWRxOaz+o5sxQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/messaging-interop-types": "0.2.5", + "@firebase/util": "1.15.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.27.tgz", + "integrity": "sha512-JNOiu1PPgdHzEPEtoFiNxQuu0x9bm4bfETSQCpGfcTlgWkhlSK7uh7nlsjC10TQLUNgYetLmuutaYTh8aeYLVA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/messaging": "0.13.0", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.5.tgz", + "integrity": "sha512-tUEKnaAP2Y/MNIqgnriPpV6e5l13Vs/+p2yrd6NGlncPJT9O3a8muYZtdnWe+IJ4fgKLHJVC79n/asxk/N5Msw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/@firebase/performance": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.12.tgz", + "integrity": "sha512-fe7nV8teUU3OBHlMUZ9Lw4gLhCW2k4m5Uc3pfWGV+fl8uwJQBGp9Q3lqsJ+HSrFu3Q2pJyLAgrClPGSKyDeYgQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.25.tgz", + "integrity": "sha512-q6NjTXpIPoFuUmCmMN/maCdTgzT6aExs9xZo+PxfVLj6uLVGvpyAD6XWjmcrb7jChsFBYbq7E5dyNDF7Zhy9kA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/performance": "0.7.12", + "@firebase/performance-types": "0.2.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.4.tgz", + "integrity": "sha512-kJSEk7b0uhpcPRyL4SQ/GPujLqk52XNKcXlnsKDbWGAb9vugcLvOU3u6zfEdwd+d8hWJb5S5ZizV1JFFI0nkKg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.4.tgz", + "integrity": "sha512-lslywR5lGvHWTu4z/MPoYs3UwS3CKdeY+ELXY87087VsOpBpkD+9Orra23tA9GW683arPTDOM3CM6eKmtiOO3g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.25.tgz", + "integrity": "sha512-FnA5S4IxFJAAFrCnYzWlO0FCaizlYdqhe42ygFMA+wE/mUP+w36iXzHyKj1OO1A+2gyMFjeRHyg8HhkJ6c5vRA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/remote-config": "0.8.4", + "@firebase/remote-config-types": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.1.tgz", + "integrity": "sha512-cX/1LT6KQwkXzck2eSzeKnuvXZCyr8qaPpDcikoJs7jmI+oBOXixpDLeDtWj1U6GNMkIoXrEDNoyT2Ypcyp5/A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.3.tgz", + "integrity": "sha512-YX4/YL6P6/fufSSeGnVhjWddcIXbFq2cWIhMKFTZo1E/Rtcl2mJj/BYUQTwJfcE1Tl8un1FOya4L05jcSLN/Eg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.3.tgz", + "integrity": "sha512-gruVqjtUGX8tEoeNbaWXZm0Zfcfcb7fvmDmBxV8yPAbWvExRnZYLO2+qw9idxNE7BvPXt5csyjSYHy//dAizxw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/storage": "0.14.3", + "@firebase/storage-types": "0.8.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.4.tgz", + "integrity": "sha512-BT7cwxJOx8SWwlQfrlC+bD/Sk3Cw+1odCi8UZNFNWTVZoPsBnA5W+mqtZzVnvsdJpXCFGSGQ7R7vOR6dtM/BRA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.1.tgz", + "integrity": "sha512-LUdM4Wg7YM9Pq/49nGYySJA0CSQEKnGffFzWV8+6gXN7mGxn+FL1IqvFbuZUtAQcfZgHYDwCE1wwlK7rB7gl2g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.6.tgz", + "integrity": "sha512-Vr/Mqu79dMwGRAyGbJ4uN4+BtXB3/mRTdzetD1daWNeG8QaWuzhhbG77GltO5c0yYmYls8i250iX73624GJd7Q==", + "license": "Apache-2.0" + }, "node_modules/@floating-ui/core": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", @@ -12884,6 +13535,66 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/proto-loader/node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "dev": true, @@ -17511,6 +18222,15 @@ "node": ">= 8" } }, + "node_modules/@notifee/react-native": { + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/@notifee/react-native/-/react-native-9.1.8.tgz", + "integrity": "sha512-Az/dueoPerJsbbjRxu8a558wKY+gONUrfoy3Hs++5OqbeMsR0dYe6P+4oN6twrLFyzAhEA1tEoZRvQTFDRmvQg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/@npmcli/config": { "version": "6.4.0", "dev": true, @@ -19500,19 +20220,24 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -19532,7 +20257,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause" }, "node_modules/@radix-ui/colors": { @@ -24214,6 +24941,40 @@ "react-native": "*" } }, + "node_modules/@react-native-firebase/app": { + "version": "24.1.1", + "resolved": "https://registry.npmjs.org/@react-native-firebase/app/-/app-24.1.1.tgz", + "integrity": "sha512-NKCUgBeilQvchHQeKwB6RuE3Ne2AZMlClXjJBVySkCAF2FNO73ML2brqrAyegRkuVswtqHsB6wAgypmP82WnnA==", + "license": "Apache-2.0", + "dependencies": { + "firebase": "12.14.0" + }, + "peerDependencies": { + "expo": ">=47.0.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@react-native-firebase/messaging": { + "version": "24.1.1", + "resolved": "https://registry.npmjs.org/@react-native-firebase/messaging/-/messaging-24.1.1.tgz", + "integrity": "sha512-tMb75+4ehbQshlH+PifbxKbP+pyDgK/dfBIvlFJeDVEAbdaM2nVpMkVyV6s5WJ3WRPUt6v+hyU1P3krQPYhkig==", + "license": "Apache-2.0", + "peerDependencies": { + "@react-native-firebase/app": "24.1.1", + "expo": ">=47.0.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, "node_modules/@react-native/assets-registry": { "version": "0.79.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz", @@ -24982,10 +25743,6 @@ "semver": "bin/semver.js" } }, - "node_modules/@react-native/normalize-color": { - "version": "2.1.0", - "license": "MIT" - }, "node_modules/@react-native/normalize-colors": { "version": "0.74.85", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.85.tgz", @@ -54385,15 +55142,6 @@ "node": ">= 0.6" } }, - "node_modules/deprecated-react-native-prop-types": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "@react-native/normalize-color": "*", - "invariant": "*", - "prop-types": "*" - } - }, "node_modules/dequal": { "version": "2.0.3", "license": "MIT", @@ -60322,6 +61070,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "license": "Apache-2.0", @@ -60786,6 +61546,42 @@ "node": ">=8" } }, + "node_modules/firebase": { + "version": "12.14.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.14.0.tgz", + "integrity": "sha512-aEZ/lniDR1hOCYpx/x/V8Nrrqq9pepKDNkqP/4WGZFC69gTv6F59Z4/54W/SUP4L/hFlrRNmWj35aweQq+IHow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.13.0", + "@firebase/analytics": "0.10.22", + "@firebase/analytics-compat": "0.2.28", + "@firebase/app": "0.14.13", + "@firebase/app-check": "0.11.4", + "@firebase/app-check-compat": "0.4.4", + "@firebase/app-compat": "0.5.13", + "@firebase/app-types": "0.9.5", + "@firebase/auth": "1.13.2", + "@firebase/auth-compat": "0.6.7", + "@firebase/data-connect": "0.7.1", + "@firebase/database": "1.1.3", + "@firebase/database-compat": "2.1.4", + "@firebase/firestore": "4.15.0", + "@firebase/firestore-compat": "0.4.10", + "@firebase/functions": "0.13.5", + "@firebase/functions-compat": "0.4.5", + "@firebase/installations": "0.6.22", + "@firebase/installations-compat": "0.2.22", + "@firebase/messaging": "0.13.0", + "@firebase/messaging-compat": "0.2.27", + "@firebase/performance": "0.7.12", + "@firebase/performance-compat": "0.2.25", + "@firebase/remote-config": "0.8.4", + "@firebase/remote-config-compat": "0.2.25", + "@firebase/storage": "0.14.3", + "@firebase/storage-compat": "0.4.3", + "@firebase/util": "1.15.1" + } + }, "node_modules/fix-hmr": { "version": "1.0.2", "license": "MIT", @@ -65332,6 +66128,12 @@ "version": "1.0.0", "license": "ISC" }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "dev": true, @@ -70657,7 +71459,6 @@ }, "node_modules/lodash.camelcase": { "version": "4.3.0", - "dev": true, "license": "MIT" }, "node_modules/lodash.clonedeep": { @@ -85576,23 +86377,6 @@ "prop-types": "^15.5.10" } }, - "node_modules/react-native-fs": { - "version": "2.20.0", - "license": "MIT", - "dependencies": { - "base-64": "^0.1.0", - "utf8": "^3.0.0" - }, - "peerDependencies": { - "react-native": "*", - "react-native-windows": "*" - }, - "peerDependenciesMeta": { - "react-native-windows": { - "optional": true - } - } - }, "node_modules/react-native-get-random-values": { "version": "1.10.0", "license": "MIT", @@ -85729,13 +86513,6 @@ } } }, - "node_modules/react-native-radial-gradient": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "deprecated-react-native-prop-types": "^2.3.0" - } - }, "node_modules/react-native-randombytes": { "version": "3.6.1", "license": "ISC", @@ -103252,6 +104029,12 @@ "node": ">= 8" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/web-worker": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", @@ -105964,6 +106747,29 @@ "node": ">=4.0.0" } }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/websocket/node_modules/debug": { "version": "2.6.9", "license": "MIT", @@ -118920,7 +119726,7 @@ }, "packages/mobile": { "name": "@audius/mobile", - "version": "1.5.185", + "version": "1.5.187", "dependencies": { "@amplitude/analytics-react-native": "1.4.11", "@audius/common": "*", @@ -118930,6 +119736,7 @@ "@audius/sdk": "*", "@bravemobile/react-native-code-push": "^12.3.2", "@coinflowlabs/react-native": "4.5.2", + "@dr.pogodin/react-native-fs": "2.32.1", "@ebay/nice-modal-react": "^1.2.13", "@emotion/native": "^11.11.0", "@emotion/react": "11.14.0", @@ -118938,6 +119745,7 @@ "@hcaptcha/react-native-hcaptcha": "1.6.0", "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.0", + "@notifee/react-native": "^9.1.8", "@optimizely/optimizely-sdk": "4.9.2", "@peculiar/webcrypto": "1.4.3", "@react-native-async-storage/async-storage": "^2.0.0", @@ -118948,6 +119756,8 @@ "@react-native-community/netinfo": "^11.3.3", "@react-native-community/slider": "4.5.0", "@react-native-documents/picker": "^10.1.2", + "@react-native-firebase/app": "^24.1.1", + "@react-native-firebase/messaging": "^24.1.1", "@react-native-masked-view/masked-view": "^0.3.2", "@react-navigation/bottom-tabs": "7.19.2", "@react-navigation/core": "7.22.1", @@ -118998,7 +119808,6 @@ "react-native-draggable-flatlist": "4.0.1", "react-native-dynamic": "1.0.0", "react-native-fast-crypto": "3.0.0", - "react-native-fs": "^2.18.0", "react-native-gesture-handler": "2.25.0", "react-native-get-random-values": "1.10.0", "react-native-gifted-charts": "1.4.46", @@ -119011,15 +119820,12 @@ "react-native-intersection-observer": "0.2.1", "react-native-keyboard-aware-scroll-view": "0.9.5", "react-native-keyboard-controller": "1.19.0", - "react-native-linear-gradient": "2.8.3", "react-native-markdown-display": "7.0.2", "react-native-modal": "^13.0.0", "react-native-modal-datetime-picker": "18.0.0", - "react-native-notifications": "5.1.0", "react-native-pager-view": "6.7.1", "react-native-permissions": "5.4.4", "react-native-qrcode-svg": "6.2.0", - "react-native-radial-gradient": "1.1.4", "react-native-randombytes": "3.6.1", "react-native-reanimated": "~3.19.5", "react-native-restart": "0.0.27", @@ -121233,14 +122039,6 @@ "react-native": "*" } }, - "packages/mobile/node_modules/react-native-linear-gradient": { - "version": "2.8.3", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "packages/mobile/node_modules/react-native-markdown-display": { "version": "7.0.2", "license": "MIT", @@ -121267,14 +122065,6 @@ "react-native": ">=0.65.0" } }, - "packages/mobile/node_modules/react-native-notifications": { - "version": "5.1.0", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "packages/mobile/node_modules/react-native-qrcode-svg": { "version": "6.2.0", "license": "MIT", diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index bbac1531a87..4aa27585396 100755 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -134,7 +134,7 @@ android { // versionCode is automatically incremented in CI versionCode 1 // Make sure this is above the currently released Android version in the play store if your changes touch native code: - versionName "1.1.535" + versionName "1.1.536" resValue "string", "build_config_package", "co.audius.app" resConfigs "en" } @@ -194,7 +194,6 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.0' implementation "androidx.appcompat:appcompat:1.0.0" - implementation project(':react-native-notifications') implementation platform('com.google.firebase:firebase-bom:31.1.1') implementation 'com.google.firebase:firebase-messaging' implementation 'com.google.firebase:firebase-analytics' diff --git a/packages/mobile/android/app/src/main/AndroidManifest.xml b/packages/mobile/android/app/src/main/AndroidManifest.xml index 8ca6c1c1ab5..a769bac310a 100755 --- a/packages/mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/mobile/android/app/src/main/AndroidManifest.xml @@ -92,6 +92,11 @@ + + + if (subproject.name == 'dr.pogodin_react-native-fs' && findProperty('newArchEnabled') != 'true') { + subproject.plugins.withId('com.android.library') { + subproject.android.sourceSets.main.java.srcDirs += ["${subproject.projectDir}/generated/java"] + } + } +} + apply plugin: "com.facebook.react.rootproject" allprojects { @@ -39,4 +56,4 @@ allprojects { dirs "$rootDir/libs" } } -} \ No newline at end of file +} diff --git a/packages/mobile/android/settings.gradle b/packages/mobile/android/settings.gradle index 7b69b74c854..db9cbaba190 100755 --- a/packages/mobile/android/settings.gradle +++ b/packages/mobile/android/settings.gradle @@ -4,15 +4,9 @@ extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autoli rootProject.name = 'AudiusReactNative' include ':react-native-randombytes' project(':react-native-randombytes').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-randombytes/android') -include ':react-native-fs' -project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-fs/android') -include ':react-native-radial-gradient' -project(':react-native-radial-gradient').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-radial-gradient/android') include ':react-native-config' project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android') include ':react-native-google-cast' project(':react-native-google-cast').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-google-cast/android') -include ':react-native-notifications' -project(':react-native-notifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/lib/android/app') include ':app' includeBuild('../../../node_modules/@react-native/gradle-plugin') diff --git a/packages/mobile/index.js b/packages/mobile/index.js index 4911004dffd..8519a4eb676 100644 --- a/packages/mobile/index.js +++ b/packages/mobile/index.js @@ -20,6 +20,10 @@ import ViewReactNativeStyleAttributes from 'react-native/Libraries/Components/Vi ViewReactNativeStyleAttributes.scaleY = true import { AppRegistry, LogBox, Text, TextInput } from 'react-native' import TrackPlayer from 'react-native-track-player' +import { + getMessaging, + setBackgroundMessageHandler +} from '@react-native-firebase/messaging' import { Crypto } from '@peculiar/webcrypto' import { name as appName } from './app.json' @@ -52,3 +56,7 @@ TextInput.defaultProps.allowFontScaling = false AppRegistry.registerComponent(appName, () => App) TrackPlayer.registerPlaybackService(() => require('./audio-service')) +// On Android, firebase starts a headless JS task for each push that arrives +// while the app is in the background. The OS displays the push itself, so +// there is nothing to do here. +setBackgroundMessageHandler(getMessaging(), async () => {}) diff --git a/packages/mobile/ios/AudiusReactNative.xcodeproj/project.pbxproj b/packages/mobile/ios/AudiusReactNative.xcodeproj/project.pbxproj index 5eca50b1fcb..b4fbde5fe32 100644 --- a/packages/mobile/ios/AudiusReactNative.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/AudiusReactNative.xcodeproj/project.pbxproj @@ -261,6 +261,7 @@ CBD3B687421F14165031EAF0 /* [CP] Embed Pods Frameworks */, B73C21C532E1201FEFBAECDA /* [CP] Copy Pods Resources */, 23A43AB82E37E73000CDCDF2 /* Embed Foundation Extensions */, + CB6D10CC025B9D671A7354C5 /* [CP-User] [RNFB] Core Configuration */, ); buildRules = ( ); @@ -434,6 +435,19 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AudiusReactNative/Pods-AudiusReactNative-resources.sh\"\n"; showEnvVarsInLog = 0; }; + CB6D10CC025B9D671A7354C5 /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"note: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"note: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"note: -> RNFB build script started\"\necho \"note: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"note: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"note: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n if ! _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\"); then\n echo \"error: Failed to parse firebase.json, check for syntax errors.\"\n exit 1\n fi\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"error: python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"note: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"note: <- RNFB build script finished\"\n"; + }; CBD3B687421F14165031EAF0 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/packages/mobile/ios/AudiusReactNative/AppDelegate.mm b/packages/mobile/ios/AudiusReactNative/AppDelegate.mm index c82bdf3a6ff..d985f2a5acb 100644 --- a/packages/mobile/ios/AudiusReactNative/AppDelegate.mm +++ b/packages/mobile/ios/AudiusReactNative/AppDelegate.mm @@ -8,7 +8,8 @@ #import #import #import -#import "RNNotifications.h" +#import +#import "NotifeeCore+UNUserNotificationCenter.h" @implementation AppDelegate { NSDictionary *_launchOptions; @@ -17,7 +18,12 @@ @implementation AppDelegate { - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [RNNotifications startMonitorNotifications]; + [FIRApp configure]; + // Our iOS pushes come straight from APNs (SNS), not FCM. Firebase messaging + // only reports taps on FCM pushes and passes the rest to the delegate it + // replaced, so notifee has to be that delegate to report them to JS. + // Both libraries install themselves after launch, so make notifee go first. + [[NotifeeCoreUNUserNotificationCenter instance] observe]; NSString *receiverAppID = @"222B31C8"; GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc] initWithApplicationID:receiverAppID]; GCKCastOptions* options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria]; @@ -72,9 +78,10 @@ - (UIWindow *)loadReactNativeWindowInScene:(UIWindowScene *)scene return window; } -// Scene apps get the launch URL, user activity and notification through the -// connection options rather than launchOptions. Linking.getInitialURL and -// Notifications.getInitialNotification read them from the bridge's launchOptions. +// Scene apps get the launch URL and user activity through the connection +// options rather than launchOptions. Linking.getInitialURL reads them from the +// bridge's launchOptions. A tapped notification needs no copy: the notification +// center delegate still receives it at launch. - (NSDictionary *)launchOptionsWithConnectionOptions:(UISceneConnectionOptions *)connectionOptions { NSMutableDictionary *launchOptions = [NSMutableDictionary dictionaryWithDictionary:_launchOptions ?: @{}]; @@ -92,11 +99,6 @@ - (NSDictionary *)launchOptionsWithConnectionOptions:(UISceneConnectionOptions * }; } - NSDictionary *notification = connectionOptions.notificationResponse.notification.request.content.userInfo; - if (notification != nil && launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] == nil) { - launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] = notification; - } - return launchOptions; } @@ -120,16 +122,4 @@ - (NSURL *)bundleURL #endif } -- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { - [RNNotifications didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; -} - -- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { - [RNNotifications didFailToRegisterForRemoteNotificationsWithError:error]; -} - -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { - [RNNotifications didReceiveBackgroundNotification:userInfo withCompletionHandler:completionHandler]; -} - @end diff --git a/packages/mobile/ios/AudiusReactNative/Info.plist b/packages/mobile/ios/AudiusReactNative/Info.plist index 60fe39b4f65..244cdfe018c 100644 --- a/packages/mobile/ios/AudiusReactNative/Info.plist +++ b/packages/mobile/ios/AudiusReactNative/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.199 + 1.1.200 CFBundleSignature ???? CFBundleURLTypes diff --git a/packages/mobile/ios/Podfile b/packages/mobile/ios/Podfile index c5d685c23e7..2c519c4ca74 100644 --- a/packages/mobile/ios/Podfile +++ b/packages/mobile/ios/Podfile @@ -31,12 +31,16 @@ project 'AudiusReactNative', 'Prod.Release' => :release target 'AudiusReactNative' do - pod 'SRSRadialGradient', :path => '../../../node_modules/react-native-radial-gradient/ios' pod 'ffmpeg-kit-ios-full-gpl', :podspec => './ffmpeg-kit-ios-full-gpl.podspec' pod 'ffmpeg-kit-react-native', :subspecs => ['full-gpl'], :podspec => '../../../node_modules/ffmpeg-kit-react-native/ffmpeg-kit-react-native.podspec' pod 'nSure' + # Firebase's Swift pods import these, and pods link statically here, so they + # need module maps. + pod 'GoogleUtilities', :modular_headers => true + pod 'nanopb', :modular_headers => true + config = use_native_modules! use_react_native!( @@ -140,6 +144,24 @@ target 'AudiusReactNative' do end end + # SwiftAudioEx 1.x only shifts the current index for an insert in front + # of the current item when the queue already has more than one item. With + # a single item, inserting at 0 leaves the index one slot early, so the + # player replays the current track instead of advancing. Fixed upstream + # on main but not released. Fail if the line changes so an update can't + # silently drop the patch. + queue_manager = File.join(__dir__, 'Pods/SwiftAudioEx/Sources/SwiftAudioEx/QueueManager.swift') + buggy_line = 'if (self.items.count > 1 && currentIndex >= index) {' + fixed_line = 'if (self.items.count > 0 && currentIndex >= index) {' + text = File.read(queue_manager) + if text.include?(buggy_line) + File.chmod(0644, queue_manager) + File.write(queue_manager, text.sub(buggy_line, fixed_line)) + Pod::UI.puts "Patched SwiftAudioEx QueueManager.swift: shift current index when inserting into a one-item queue".green + elsif !text.include?(fixed_line) + raise "SwiftAudioEx QueueManager.swift no longer contains '#{buggy_line}'. Check whether the insert index fix is still needed and update the Podfile patch." + end + end end diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index f9c1ce8ba5f..7d3f60ecc4c 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -3,23 +3,115 @@ PODS: - React-Core - Base64 (1.1.2) - boost (1.84.0) - - BVLinearGradient (2.8.3): - - React-Core - CodePush (12.3.2): - Base64 (~> 1.1) - JWT (~> 3.0.0-beta.12) - React-Core - SSZipArchive (~> 2.5.5) - DoubleConversion (1.1.6) + - dr-pogodin-react-native-fs (2.32.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - fast_float (6.1.4) - FBLazyVector (0.79.5) - ffmpeg-kit-ios-full-gpl (6.0) - ffmpeg-kit-react-native/full-gpl (6.0.2): - ffmpeg-kit-ios-full-gpl (= 6.0) - React-Core + - Firebase/CoreOnly (12.10.0): + - FirebaseCore (~> 12.10.0) + - Firebase/Messaging (12.10.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 12.10.0) + - FirebaseCore (12.10.0): + - FirebaseCoreInternal (~> 12.10.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.10.0): + - FirebaseCore (~> 12.10.0) + - FirebaseCoreInternal (12.10.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseInstallations (12.10.0): + - FirebaseCore (~> 12.10.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (12.10.0): + - FirebaseCore (~> 12.10.0) + - FirebaseInstallations (~> 12.10.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Reachability (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) - fmt (11.0.2) - glog (0.3.5) - google-cast-sdk-dynamic-xcframework-no-bluetooth (4.7.1) + - GoogleDataTransport (10.1.1): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities (8.1.2): + - GoogleUtilities/AppDelegateSwizzler (= 8.1.2) + - GoogleUtilities/Environment (= 8.1.2) + - GoogleUtilities/Logger (= 8.1.2) + - GoogleUtilities/MethodSwizzler (= 8.1.2) + - GoogleUtilities/Network (= 8.1.2) + - "GoogleUtilities/NSData+zlib (= 8.1.2)" + - GoogleUtilities/Privacy (= 8.1.2) + - GoogleUtilities/Reachability (= 8.1.2) + - GoogleUtilities/SwizzlerTestHelpers (= 8.1.2) + - GoogleUtilities/UserDefaults (= 8.1.2) + - GoogleUtilities/AppDelegateSwizzler (8.1.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (8.1.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.2) + - GoogleUtilities/Reachability (8.1.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/SwizzlerTestHelpers (8.1.2): + - GoogleUtilities/MethodSwizzler + - GoogleUtilities/UserDefaults (8.1.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy - hermes-engine (0.79.5): - hermes-engine/Pre-built (= 0.79.5) - hermes-engine/Pre-built (0.79.5) @@ -51,6 +143,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - nSure (1.3.13) - PromisesObjC (2.4.0) - RCT-Folly (2024.11.18.00): @@ -1497,8 +1594,6 @@ PODS: - Yoga - react-native-netinfo (11.4.1): - React-Core - - react-native-notifications (5.1.0): - - React-Core - react-native-pager-view (6.7.1): - DoubleConversion - glog @@ -1553,9 +1648,9 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-track-player (4.0.1): + - react-native-track-player (4.1.2): - React-Core - - SwiftAudioEx (= 1.0.0) + - SwiftAudioEx (= 1.1.0) - react-native-version-number (0.3.6): - React - react-native-video (6.18.0): @@ -1987,8 +2082,9 @@ PODS: - Yoga - RNDateTimePicker (8.3.0): - React-Core - - RNFlashList (1.8.3): + - RNFBApp (24.1.1): - DoubleConversion + - Firebase/CoreOnly (= 12.10.0) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -2011,8 +2107,57 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNFS (2.20.0): + - RNFBMessaging (24.1.1): + - DoubleConversion + - Firebase/Messaging (= 12.10.0) + - FirebaseCoreExtension + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNFBApp + - Yoga + - RNFlashList (1.8.3): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - RNGestureHandler (2.25.0): - DoubleConversion - glog @@ -2046,6 +2191,11 @@ PODS: - React-Core - React-RCTImage - TOCropViewController (~> 2.7.4) + - RNNotifee (9.1.8): + - React-Core + - RNNotifee/NotifeeCore (= 9.1.8) + - RNNotifee/NotifeeCore (9.1.8): + - React-Core - RNPermissions (5.4.4): - React-Core - RNReactNativeHapticFeedback (2.3.3): @@ -2264,10 +2414,8 @@ PODS: - SnapSDK/SCSDKLoginKit (1.15.0): - SnapSDK/SCSDKCoreKit - SocketRocket (0.7.1) - - SRSRadialGradient (1.1.4): - - React - SSZipArchive (2.5.5) - - SwiftAudioEx (1.0.0) + - SwiftAudioEx (1.1.0) - tiktok-opensdk-react-native (0.10.7): - DoubleConversion - glog @@ -2308,17 +2456,19 @@ PODS: DEPENDENCIES: - "amplitude-react-native (from `../../../node_modules/@amplitude/analytics-react-native`)" - boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`) - - BVLinearGradient (from `../node_modules/react-native-linear-gradient`) - "CodePush (from `../../../node_modules/@bravemobile/react-native-code-push`)" - DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - "dr-pogodin-react-native-fs (from `../../../node_modules/@dr.pogodin/react-native-fs`)" - fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`) - ffmpeg-kit-ios-full-gpl (from `./ffmpeg-kit-ios-full-gpl.podspec`) - ffmpeg-kit-react-native/full-gpl (from `../../../node_modules/ffmpeg-kit-react-native/ffmpeg-kit-react-native.podspec`) - fmt (from `../../../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`) + - GoogleUtilities - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - lottie-react-native (from `../node_modules/lottie-react-native`) + - nanopb - nSure - RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) @@ -2364,13 +2514,12 @@ DEPENDENCIES: - react-native-in-app-review (from `../node_modules/react-native-in-app-review`) - react-native-keyboard-controller (from `../../../node_modules/react-native-keyboard-controller`) - "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)" - - react-native-notifications (from `../node_modules/react-native-notifications`) - react-native-pager-view (from `../../../node_modules/react-native-pager-view`) - react-native-randombytes (from `../../../node_modules/react-native-randombytes`) - react-native-restart (from `../node_modules/react-native-restart`) - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - - react-native-track-player (from `../node_modules/react-native-track-player`) + - react-native-track-player (from `../../../node_modules/react-native-track-player`) - react-native-version-number (from `../../../node_modules/react-native-version-number`) - react-native-video (from `../../../node_modules/react-native-video`) - react-native-view-shot (from `../node_modules/react-native-view-shot`) @@ -2411,10 +2560,12 @@ DEPENDENCIES: - "RNCClipboard (from `../../../node_modules/@react-native-clipboard/clipboard`)" - "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)" - "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)" + - "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)" + - "RNFBMessaging (from `../../../node_modules/@react-native-firebase/messaging`)" - "RNFlashList (from `../../../node_modules/@shopify/flash-list`)" - - RNFS (from `../../../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) + - "RNNotifee (from `../../../node_modules/@notifee/react-native`)" - RNPermissions (from `../../../node_modules/react-native-permissions`) - RNReactNativeHapticFeedback (from `../../../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) @@ -2423,16 +2574,24 @@ DEPENDENCIES: - RNSVG (from `../node_modules/react-native-svg`) - RNZipArchive (from `../../../node_modules/react-native-zip-archive`) - "snap-kit-react-native (from `../../../node_modules/@snapchat/snap-kit-react-native`)" - - SRSRadialGradient (from `../../../node_modules/react-native-radial-gradient/ios`) - tiktok-opensdk-react-native (from `../../../node_modules/tiktok-opensdk-react-native`) - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: - Base64 + - Firebase + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseInstallations + - FirebaseMessaging - google-cast-sdk-dynamic-xcframework-no-bluetooth + - GoogleDataTransport + - GoogleUtilities - JWT - lottie-ios + - nanopb - nSure - PromisesObjC - SnapSDK @@ -2448,12 +2607,12 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@amplitude/analytics-react-native" boost: :podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec" - BVLinearGradient: - :path: "../node_modules/react-native-linear-gradient" CodePush: :path: "../../../node_modules/@bravemobile/react-native-code-push" DoubleConversion: :podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + dr-pogodin-react-native-fs: + :path: "../../../node_modules/@dr.pogodin/react-native-fs" fast_float: :podspec: "../../../node_modules/react-native/third-party-podspecs/fast_float.podspec" FBLazyVector: @@ -2555,8 +2714,6 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-keyboard-controller" react-native-netinfo: :path: "../../../node_modules/@react-native-community/netinfo" - react-native-notifications: - :path: "../node_modules/react-native-notifications" react-native-pager-view: :path: "../../../node_modules/react-native-pager-view" react-native-randombytes: @@ -2568,7 +2725,7 @@ EXTERNAL SOURCES: react-native-slider: :path: "../node_modules/@react-native-community/slider" react-native-track-player: - :path: "../node_modules/react-native-track-player" + :path: "../../../node_modules/react-native-track-player" react-native-version-number: :path: "../../../node_modules/react-native-version-number" react-native-video: @@ -2649,14 +2806,18 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-masked-view/masked-view" RNDateTimePicker: :path: "../node_modules/@react-native-community/datetimepicker" + RNFBApp: + :path: "../../../node_modules/@react-native-firebase/app" + RNFBMessaging: + :path: "../../../node_modules/@react-native-firebase/messaging" RNFlashList: :path: "../../../node_modules/@shopify/flash-list" - RNFS: - :path: "../../../node_modules/react-native-fs" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNImageCropPicker: :path: "../node_modules/react-native-image-crop-picker" + RNNotifee: + :path: "../../../node_modules/@notifee/react-native" RNPermissions: :path: "../../../node_modules/react-native-permissions" RNReactNativeHapticFeedback: @@ -2673,8 +2834,6 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-zip-archive" snap-kit-react-native: :path: "../../../node_modules/@snapchat/snap-kit-react-native" - SRSRadialGradient: - :path: "../../../node_modules/react-native-radial-gradient/ios" tiktok-opensdk-react-native: :path: "../../../node_modules/tiktok-opensdk-react-native" Yoga: @@ -2684,20 +2843,29 @@ SPEC CHECKSUMS: amplitude-react-native: 9d57e1bcc4175039e36283390aa3daeaea9441a5 Base64: cecfb41a004124895a7bcee567a89bae5a89d49b boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 CodePush: f52aac93456bc76d8219ca3673cc7ce81470429d DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb + dr-pogodin-react-native-fs: 69314b5d4e9f561657febeae7a250c2bb8561874 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52 ffmpeg-kit-ios-full-gpl: 7d416729f7b3604b64cee2a752c42608a8d228e0 ffmpeg-kit-react-native: 3cea88c9c5cfad62e1465279ea7d800dfbba3b00 + Firebase: 99f203d3a114c6ba591f3b32263a9626e450af65 + FirebaseCore: f4428e22415ea3b3eca652c2098413dabf2a23a9 + FirebaseCoreExtension: 3473a6ec16a91aee29fb75994a86c0220d2cddf3 + FirebaseCoreInternal: e7bbaeb00ab73011298f35ed223aa7371e212948 + FirebaseInstallations: 047343aa91fd6a1ebfa3eb374ddecf36a8aaddfd + FirebaseMessaging: ed18fb50634e6e85b5d3e77e628c21e2c928cc53 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 google-cast-sdk-dynamic-xcframework-no-bluetooth: 1fa9e267df3fd6f8a1c6e3345142ca5286297968 + GoogleDataTransport: a24e58982ab3ba2f64d79613e027fe7f57e88539 + GoogleUtilities: 766ace00c6b10d8148408f329d10c4f051931850 hermes-engine: f03b0e06d3882d71e67e45b073bb827da1a21aae JWT: ef71dfb03e1f842081e64dc42eef0e164f35d251 lottie-ios: a881093fab623c467d3bce374367755c272bdd59 lottie-react-native: 173db75996ba0ebc176424e04cfc0cc0175b307d + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nSure: 2fc3fc973c44aa0be9a3446f84cb514adc475205 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 @@ -2742,13 +2910,12 @@ SPEC CHECKSUMS: react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4 react-native-keyboard-controller: c8c8a6ce30420ca67fae84dd1a9f9a861a988e2a react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac - react-native-notifications: 4601a5a8db4ced6ae7cfc43b44d35fe437ac50c4 react-native-pager-view: e4cf30bc28e3ccf864f6cfb90590d8cea13b7393 react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 react-native-safe-area-context: e48706fb09fde89f82e12bfb49b140cfdcb761f7 react-native-slider: f7d96d39d20d367a20d7af5bdae138aed3b2e75f - react-native-track-player: 97d76dbbd35f27cc709e5f04540615e54264b3f9 + react-native-track-player: 24ecd55d64da8a6471ba96484b67bc9cee6fc629 react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f react-native-video: 5efc3c2185906e2c49c81e7e2c763193f5c5e419 react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 @@ -2789,10 +2956,12 @@ SPEC CHECKSUMS: RNCClipboard: dfeb43751adff21e588657b5b6c888c72f3aa68e RNCMaskedView: dc63cbc7b2f614b8b4526e11ed677b36e92cd9fb RNDateTimePicker: a793ed8822283f576dd0a205a0916c5098c2611f + RNFBApp: ef18c25022685d22dfc058983c2f287b1838e848 + RNFBMessaging: fae02a2b3354255cf16e7c598d282a6d9531e5c7 RNFlashList: c68de6a14cce0a262b0a4fabdea690d9f3361063 - RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 1070e4e691b23028309d796cc30652d9875138c7 RNImageCropPicker: 64b6bec9ec185ee59b71a93d7058816bd0e1916e + RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9 RNPermissions: 0a18cdcfdd1773b969f817ce20d47c2b74724af8 RNReactNativeHapticFeedback: b837dd570ef9c316ed9f93274a2cdb152a8d78ab RNReanimated: b61d01db005bc4114b04e7a88c3f32cbd3865dac @@ -2803,15 +2972,14 @@ SPEC CHECKSUMS: snap-kit-react-native: 751006199818fccb38c8a01196495167bc25e9fb SnapSDK: 1e68aad748e080f49e3802559b3b76026666ef62 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - SRSRadialGradient: 8363f6c3ce63636f5ffee74a7016f5c84a6df0a1 SSZipArchive: c69881e8ac5521f0e622291387add5f60f30f3c4 - SwiftAudioEx: 6f511018b7a0fdfd14ed1bb4081f953588245cc0 + SwiftAudioEx: f6aa653770f3a0d3851edaf8d834a30aee4a7646 tiktok-opensdk-react-native: f6b1a34bb4c1889c0dae5838effe484c673ea0c0 TikTokOpenSDKCore: e6f34e48bd6e85e4d94f9c04782c13d5defafb55 TikTokOpenShareSDK: a7da017bc66c28d0aefea9342c0cfcc7e52ea2b7 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 Yoga: adb397651e1c00672c12e9495babca70777e411e -PODFILE CHECKSUM: 7750e6bb179cd027ed6cf7522c2da1f14bef882b +PODFILE CHECKSUM: 2f4f4e3fbbf9bb9553299f2691874304461629c9 COCOAPODS: 1.15.2 diff --git a/packages/mobile/ios/PrivacyInfo.xcprivacy b/packages/mobile/ios/PrivacyInfo.xcprivacy index 906f4fa6116..657971cb2ec 100644 --- a/packages/mobile/ios/PrivacyInfo.xcprivacy +++ b/packages/mobile/ios/PrivacyInfo.xcprivacy @@ -6,35 +6,39 @@ NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons - C617.1 - 3B52.1 + CA92.1 + 1C8F.1 + C56D.1 NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyAccessedAPITypeReasons - CA92.1 + C617.1 + 3B52.1 + 0A2A.1 NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPICategoryDiskSpace NSPrivacyAccessedAPITypeReasons - 35F9.1 + 85F4.1 + E174.1 NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPICategorySystemBootTime NSPrivacyAccessedAPITypeReasons - E174.1 + 35F9.1 diff --git a/packages/mobile/metro.config.js b/packages/mobile/metro.config.js index 025e82786d1..f31d86dfd69 100644 --- a/packages/mobile/metro.config.js +++ b/packages/mobile/metro.config.js @@ -149,9 +149,15 @@ const config = { // Aliases for '@audius/web' to allow for absolute paths ...getClientAliases(), + // react-native-gifted-charts requires this at module load + 'react-native-linear-gradient': path.resolve( + __dirname, + 'src/shims/react-native-linear-gradient.ts' + ), + // Various polyfills to enable @audius/sdk to run in react-native child_process: emptyPolyfill, - fs: resolveModule('react-native-fs'), + fs: resolveModule('@dr.pogodin/react-native-fs'), net: emptyPolyfill, tls: resolveModule('tls-browserify') }, diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 9fd700ca66a..0a7579eeb93 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@audius/mobile", - "version": "1.5.186", + "version": "1.5.187", "private": true, "scripts": { "android:dev": "ENVFILE=.env.dev turbo run android -- --mode=prodDebug", @@ -50,6 +50,7 @@ "@audius/sdk": "*", "@bravemobile/react-native-code-push": "^12.3.2", "@coinflowlabs/react-native": "4.5.2", + "@dr.pogodin/react-native-fs": "2.32.1", "@ebay/nice-modal-react": "^1.2.13", "@emotion/native": "^11.11.0", "@emotion/react": "11.14.0", @@ -58,6 +59,7 @@ "@hcaptcha/react-native-hcaptcha": "1.6.0", "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.0", + "@notifee/react-native": "^9.1.8", "@optimizely/optimizely-sdk": "4.9.2", "@peculiar/webcrypto": "1.4.3", "@react-native-async-storage/async-storage": "^2.0.0", @@ -68,6 +70,8 @@ "@react-native-community/netinfo": "^11.3.3", "@react-native-community/slider": "4.5.0", "@react-native-documents/picker": "^10.1.2", + "@react-native-firebase/app": "^24.1.1", + "@react-native-firebase/messaging": "^24.1.1", "@react-native-masked-view/masked-view": "^0.3.2", "@react-navigation/bottom-tabs": "7.19.2", "@react-navigation/core": "7.22.1", @@ -118,7 +122,6 @@ "react-native-draggable-flatlist": "4.0.1", "react-native-dynamic": "1.0.0", "react-native-fast-crypto": "3.0.0", - "react-native-fs": "^2.18.0", "react-native-gesture-handler": "2.25.0", "react-native-get-random-values": "1.10.0", "react-native-gifted-charts": "1.4.46", @@ -131,15 +134,12 @@ "react-native-intersection-observer": "0.2.1", "react-native-keyboard-aware-scroll-view": "0.9.5", "react-native-keyboard-controller": "1.19.0", - "react-native-linear-gradient": "2.8.3", "react-native-markdown-display": "7.0.2", "react-native-modal": "^13.0.0", "react-native-modal-datetime-picker": "18.0.0", - "react-native-notifications": "5.1.0", "react-native-pager-view": "6.7.1", "react-native-permissions": "5.4.4", "react-native-qrcode-svg": "6.2.0", - "react-native-radial-gradient": "1.1.4", "react-native-randombytes": "3.6.1", "react-native-reanimated": "~3.19.5", "react-native-restart": "0.0.27", diff --git a/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/BottomTabBarButton.tsx b/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/BottomTabBarButton.tsx index 2eca59fcabb..00b5fd84458 100644 --- a/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/BottomTabBarButton.tsx +++ b/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/BottomTabBarButton.tsx @@ -5,9 +5,10 @@ import { useTheme } from '@emotion/react' import type { LottieViewProps } from 'lottie-react-native' import LottieView from 'lottie-react-native' import { Pressable, StyleSheet } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import { usePrevious } from 'react-use' +import { LinearGradient } from '@audius/harmony-native' + import { BOTTOM_BAR_BUTTON_HEIGHT, BOTTOM_BAR_TOP_PADDING } from '../constants' export type BaseBottomTabBarButtonProps = { diff --git a/packages/mobile/src/components/card/Card.tsx b/packages/mobile/src/components/card/Card.tsx index cf8cab3819f..8f5df1c0111 100644 --- a/packages/mobile/src/components/card/Card.tsx +++ b/packages/mobile/src/components/card/Card.tsx @@ -3,9 +3,8 @@ import type { ComponentType, ReactNode } from 'react' import type { User } from '@audius/common/models' import type { StyleProp, TextStyle, ViewStyle } from 'react-native' import { Text, View } from 'react-native' -import type { LinearGradientProps } from 'react-native-linear-gradient' -import type { ImageProps } from '@audius/harmony-native' +import type { LinearGradientProps, ImageProps } from '@audius/harmony-native' import type { TileProps } from 'app/components/core' import { Tile } from 'app/components/core' import { UserBadges } from 'app/components/user-badges' diff --git a/packages/mobile/src/components/challenge-rewards-drawer/ReferralLinkCopyButton.tsx b/packages/mobile/src/components/challenge-rewards-drawer/ReferralLinkCopyButton.tsx index 230199cadce..8c05a3b5693 100644 --- a/packages/mobile/src/components/challenge-rewards-drawer/ReferralLinkCopyButton.tsx +++ b/packages/mobile/src/components/challenge-rewards-drawer/ReferralLinkCopyButton.tsx @@ -2,9 +2,8 @@ import { useCallback } from 'react' import Clipboard from '@react-native-clipboard/clipboard' import { Animated, View, TouchableHighlight } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' -import { IconCopy } from '@audius/harmony-native' +import { LinearGradient, IconCopy } from '@audius/harmony-native' import Text from 'app/components/text' import { usePressScaleAnimation } from 'app/hooks/usePressScaleAnimation' import { useToast } from 'app/hooks/useToast' @@ -70,9 +69,7 @@ export const ReferralLinkCopyButton = ({ > diff --git a/packages/mobile/src/components/copy-text-tile/CopyTextTile.tsx b/packages/mobile/src/components/copy-text-tile/CopyTextTile.tsx index 584a39c8888..443044791a0 100644 --- a/packages/mobile/src/components/copy-text-tile/CopyTextTile.tsx +++ b/packages/mobile/src/components/copy-text-tile/CopyTextTile.tsx @@ -2,9 +2,8 @@ import { useCallback } from 'react' import Clipboard from '@react-native-clipboard/clipboard' import { View } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' -import { IconCopy } from '@audius/harmony-native' +import { LinearGradient, IconCopy } from '@audius/harmony-native' import { Tile, Text } from 'app/components/core' import { useToast } from 'app/hooks/useToast' import { makeStyles } from 'app/styles' diff --git a/packages/mobile/src/components/core/GradientIcon.tsx b/packages/mobile/src/components/core/GradientIcon.tsx index bfa2dc14cdc..5c93856ecf1 100644 --- a/packages/mobile/src/components/core/GradientIcon.tsx +++ b/packages/mobile/src/components/core/GradientIcon.tsx @@ -1,9 +1,9 @@ import type { ComponentType } from 'react' import MaskedView from '@react-native-masked-view/masked-view' -import LinearGradient from 'react-native-linear-gradient' import type { SvgProps } from 'react-native-svg' +import { LinearGradient } from '@audius/harmony-native' import { useThemeColors } from 'app/utils/theme' type GradientIconProps = SvgProps & { diff --git a/packages/mobile/src/components/core/GradientText.tsx b/packages/mobile/src/components/core/GradientText.tsx index 51aa9d3b85e..e6b13b8e995 100644 --- a/packages/mobile/src/components/core/GradientText.tsx +++ b/packages/mobile/src/components/core/GradientText.tsx @@ -1,8 +1,8 @@ import MaskedView from '@react-native-masked-view/masked-view' import type { TextProps } from 'react-native' -import type { LinearGradientProps } from 'react-native-linear-gradient' -import LinearGradient from 'react-native-linear-gradient' +import type { LinearGradientProps } from '@audius/harmony-native' +import { LinearGradient } from '@audius/harmony-native' import Text from 'app/components/text' import { useThemeColors } from 'app/utils/theme' diff --git a/packages/mobile/src/components/core/LockedStatusBadge.tsx b/packages/mobile/src/components/core/LockedStatusBadge.tsx index f5a959bbc5e..ed91330ff7c 100644 --- a/packages/mobile/src/components/core/LockedStatusBadge.tsx +++ b/packages/mobile/src/components/core/LockedStatusBadge.tsx @@ -1,7 +1,11 @@ import { View } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' -import { IconLock, IconLockUnlocked, useTheme } from '@audius/harmony-native' +import { + LinearGradient, + IconLock, + IconLockUnlocked, + useTheme +} from '@audius/harmony-native' import { makeStyles } from 'app/styles' import { spacing } from 'app/styles/spacing' import { useColor } from 'app/utils/theme' diff --git a/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx b/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx index f6e2ea32ae9..ae2cebf6c93 100644 --- a/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx +++ b/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx @@ -14,11 +14,11 @@ import { } from '@audius/common/store' import { USDC } from '@audius/fixed-decimal' import { TouchableOpacity } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import { useDispatch, useSelector } from 'react-redux' import type { FlexProps } from '@audius/harmony-native' import { + LinearGradient, Button, Flex, IconLock, diff --git a/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx b/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx index f9a018ea10a..c7da89c78fd 100644 --- a/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx +++ b/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx @@ -4,11 +4,11 @@ import { useCurrentTrack } from '@audius/common/hooks' import { playbackSelectors, playbackRateValueMap } from '@audius/common/store' import { Genre } from '@audius/common/utils' import { Animated, Dimensions, Easing } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import TrackPlayer, { useIsPlaying } from 'react-native-track-player' import { useSelector } from 'react-redux' import { useAsync } from 'react-use' +import { LinearGradient } from '@audius/harmony-native' import { makeStyles } from 'app/styles' import { useThemeColors } from 'app/utils/theme' @@ -143,7 +143,6 @@ export const TrackingBar = (props: TrackingBarProps) => { { style={[styles.skeleton, { transform: [{ translateX: shimmerPos }] }]} > & { + /** Two or more colors, evenly spaced unless `locations` is provided. */ + colors: readonly string[] + /** Gradient start as a fraction of the box. Defaults to the top edge. */ + start?: LinearGradientPoint + /** Gradient end as a fraction of the box. Defaults to the bottom edge. */ + end?: LinearGradientPoint + /** + * Bearing in degrees (0 points up, increasing clockwise). Overrides `start` + * and `end`, and measures the box to place the gradient line like a CSS + * angle gradient. + */ + angle?: number + /** Per-color stop positions in [0, 1]; should match `colors` length. */ + locations?: readonly number[] + style?: StyleProp + children?: ReactNode +} + +// Top to bottom by default. +const DEFAULT_START: LinearGradientPoint = { x: 0.5, y: 0 } +const DEFAULT_END: LinearGradientPoint = { x: 0.5, y: 1 } + +const BORDER_RADIUS_KEYS = [ + 'borderRadius', + 'borderTopLeftRadius', + 'borderTopRightRadius', + 'borderBottomLeftRadius', + 'borderBottomRightRadius', + 'borderTopStartRadius', + 'borderTopEndRadius', + 'borderBottomStartRadius', + 'borderBottomEndRadius', + 'borderCurve' +] as const + +// The gradient line passes through the center along the bearing and is long +// enough that the perpendiculars through opposite corners meet its ends. +export const getAngleStartEnd = ( + angle: number, + width: number, + height: number +): { start: LinearGradientPoint; end: LinearGradientPoint } => { + const w = width > 0 ? width : 1 + const h = height > 0 ? height : 1 + const radians = (angle * Math.PI) / 180 + const sin = Math.sin(radians) + const cos = Math.cos(radians) + const halfLength = (w * Math.abs(sin) + h * Math.abs(cos)) / 2 + const dx = (sin * halfLength) / w + const dy = (cos * halfLength) / h + return { + start: { x: 0.5 - dx, y: 0.5 + dy }, + end: { x: 0.5 + dx, y: 0.5 - dy } + } +} + +const styles = StyleSheet.create({ + clip: { overflow: 'hidden' } +}) + +/** + * Linear gradient drawn with react-native-svg. `start` and `end` are fractions + * of the box (objectBoundingBox units), so no layout measurement is needed + * unless `angle` is set. The gradient renders behind any children and is + * clipped to the container's border radii. + */ +export const LinearGradient = (props: LinearGradientProps) => { + const { + colors, + locations, + start: startProp = DEFAULT_START, + end: endProp = DEFAULT_END, + angle, + style, + children, + onLayout, + ...other + } = props + + const [size, setSize] = useState({ width: 0, height: 0 }) + const handleLayout = useCallback( + (e: LayoutChangeEvent) => { + if (angle !== undefined) { + const { width, height } = e.nativeEvent.layout + setSize((prev) => + prev.width === width && prev.height === height + ? prev + : { width, height } + ) + } + onLayout?.(e) + }, + [angle, onLayout] + ) + + const { start, end } = + angle === undefined + ? { start: startProp, end: endProp } + : getAngleStartEnd(angle, size.width, size.height) + + // useId output is not a valid SVG id, so strip it to safe characters. + const gradientId = `linear-gradient-${useId().replace(/[^\w-]/g, '')}` + + // Clip the gradient (not the children) to the container's corner radii. + const clipStyle = useMemo(() => { + const flat = StyleSheet.flatten(style) ?? {} + const radii = Object.fromEntries( + BORDER_RADIUS_KEYS.filter((key) => flat[key] !== undefined).map((key) => [ + key, + flat[key] + ]) + ) as ViewStyle + return [StyleSheet.absoluteFill, radii, styles.clip] + }, [style]) + + return ( + + + + + + {colors.map((color, index) => ( + 1 ? index / (colors.length - 1) : 0) + } + stopColor={color} + /> + ))} + + + + + + {children} + + ) +} diff --git a/packages/mobile/src/harmony-native/components/RadialGradient/RadialGradient.tsx b/packages/mobile/src/harmony-native/components/RadialGradient/RadialGradient.tsx index 1fdf297915c..5c4f04a60c6 100644 --- a/packages/mobile/src/harmony-native/components/RadialGradient/RadialGradient.tsx +++ b/packages/mobile/src/harmony-native/components/RadialGradient/RadialGradient.tsx @@ -1,57 +1,94 @@ -import { useState } from 'react' +import { useId, useState } from 'react' import { css } from '@emotion/native' -import { Platform, View } from 'react-native' -import type { RadialGradientProps } from 'react-native-radial-gradient' -import RNRadialGradient from 'react-native-radial-gradient' +import type { ViewProps } from 'react-native' +import { StyleSheet, View } from 'react-native' +import Svg, { + Defs, + RadialGradient as SvgRadialGradient, + Rect, + Stop +} from 'react-native-svg' const fullSize = css({ height: '100%', width: '100%' }) -// RadialGradient that uses percentages instead of pixels for center and radius -// This makes RadialGradient much more useful and dynamic +export type RadialGradientProps = ViewProps & { + colors: string[] + center?: number[] + stops?: number[] + radius?: number +} + +// RadialGradient that takes its center and radius as percentages of the +// laid-out size instead of pixels. Drawn with react-native-svg behind any +// children. export const RadialGradient = (props: RadialGradientProps) => { const { - center: centerProp = [50, 50], - radius: radiusProp, + colors, + stops, + center = [50, 50], + radius = 50, style, + children, + onLayout, ...other } = props - const [{ height, width }, setDimensions] = useState({ - height: 0, - width: 0 - }) - - let center = centerProp - ? [(centerProp[0] * width) / 100, (centerProp[1] * height) / 100] - : undefined + const [{ height, width }, setDimensions] = useState({ height: 0, width: 0 }) - // We update the center prop to use x,y coordinates on iOS due to - // bug where center prop cannot be updated on iOS - if (Platform.OS === 'ios') { - center = center - ? ({ - x: center[0], - y: center[1] - } as unknown as RadialGradientProps['center']) - : undefined - } + // useId output is not a valid SVG id, so strip it to safe characters. + const gradientId = `radial-gradient-${useId().replace(/[^\w-]/g, '')}` - // Since we cant have ellipse gradients, we use the average of height and width - const radius = radiusProp - ? (radiusProp * ((height + width) / 2)) / 100 - : undefined + const cx = (center[0] * width) / 100 + const cy = (center[1] * height) / 100 + // Use the average of width and height so the gradient stays circular. + const r = (radius * ((height + width) / 2)) / 100 return ( setDimensions(e.nativeEvent.layout)} + onLayout={(e) => { + setDimensions(e.nativeEvent.layout) + onLayout?.(e) + }} > - + {width > 0 && height > 0 ? ( + + + + {colors.map((color, index) => ( + 1 ? index / (colors.length - 1) : 0) + } + stopColor={color} + /> + ))} + + + + + ) : null} + {children} ) } diff --git a/packages/mobile/src/harmony-native/components/index.ts b/packages/mobile/src/harmony-native/components/index.ts index 5e8f0475d4d..f82a532997b 100644 --- a/packages/mobile/src/harmony-native/components/index.ts +++ b/packages/mobile/src/harmony-native/components/index.ts @@ -25,6 +25,7 @@ export type { SocialButtonProps } from './Button/SocialButton/types' export * from './Button/PlainButton/PlainButton' export * from './Button/PlainButton/types' export * from './CompletionCheck/CompletionCheck' +export * from './LinearGradient/LinearGradient' export * from './RadialGradient/RadialGradient' export * from './Image/Image' export * from './Radio' diff --git a/packages/mobile/src/notifications.test.ts b/packages/mobile/src/notifications.test.ts new file mode 100644 index 00000000000..b99ed304e84 --- /dev/null +++ b/packages/mobile/src/notifications.test.ts @@ -0,0 +1,211 @@ +const mockMessaging = { + getAPNSToken: jest.fn(), + getInitialNotification: jest.fn(), + getToken: jest.fn(), + isDeviceRegisteredForRemoteMessages: jest.fn(), + onNotificationOpenedApp: jest.fn(), + onTokenRefresh: jest.fn(), + registerDeviceForRemoteMessages: jest.fn(), + requestPermission: jest.fn() +} +const mockNotifee = { + onForegroundEvent: jest.fn(), + onBackgroundEvent: jest.fn(), + setBadgeCount: jest.fn() +} +const mockStorage: Record = {} + +jest.mock('@react-native-firebase/messaging', () => ({ + getMessaging: () => 'messaging', + getAPNSToken: (...args: any[]) => mockMessaging.getAPNSToken(...args), + getInitialNotification: (...args: any[]) => + mockMessaging.getInitialNotification(...args), + getToken: (...args: any[]) => mockMessaging.getToken(...args), + isDeviceRegisteredForRemoteMessages: (...args: any[]) => + mockMessaging.isDeviceRegisteredForRemoteMessages(...args), + onNotificationOpenedApp: (...args: any[]) => + mockMessaging.onNotificationOpenedApp(...args), + onTokenRefresh: (...args: any[]) => mockMessaging.onTokenRefresh(...args), + registerDeviceForRemoteMessages: (...args: any[]) => + mockMessaging.registerDeviceForRemoteMessages(...args), + requestPermission: (...args: any[]) => + mockMessaging.requestPermission(...args) +})) +jest.mock('@notifee/react-native', () => ({ + __esModule: true, + default: mockNotifee, + EventType: { PRESS: 1, DISMISSED: 0 } +})) +jest.mock('@react-native-async-storage/async-storage', () => ({ + getItem: async (key: string) => mockStorage[key] ?? null, + setItem: async (key: string, value: string) => { + mockStorage[key] = value + }, + removeItem: async (key: string) => { + delete mockStorage[key] + } +})) +jest.mock('react-native-permissions', () => ({ + requestNotifications: jest.fn() +})) +jest.mock('@audius/common/api', () => ({ + getCurrentAccountQueryKey: () => ['account'] +})) +jest.mock('app/services/analytics', () => ({ + track: jest.fn(), + make: (e: any) => e +})) +jest.mock('app/services/audius-backend-instance', () => ({ + audiusBackendInstance: {} +})) +jest.mock('app/services/query-client', () => ({ + queryClient: { getQueryData: () => undefined } +})) +jest.mock('app/services/sdk/audius-sdk', () => ({ audiusSdk: jest.fn() })) +jest.mock('app/types/analytics', () => ({ + EventNames: { NOTIFICATIONS_OPEN_PUSH_NOTIFICATION: 'open' } +})) + +const loadNotifications = (os: 'ios' | 'android') => { + let mod: any + jest.isolateModules(() => { + jest.doMock('react-native', () => ({ Platform: { OS: os } })) + mod = require('./notifications').default + }) + return mod +} + +const iosPress = (data: Record) => ({ + type: 1, + detail: { notification: { title: 't', body: 'b', data } } +}) + +describe('PushNotifications', () => { + beforeEach(() => { + jest.clearAllMocks() + for (const key of Object.keys(mockStorage)) delete mockStorage[key] + }) + + describe('ios', () => { + it('registers the lowercase APNs token, not an FCM token', async () => { + mockMessaging.isDeviceRegisteredForRemoteMessages.mockReturnValue(false) + mockMessaging.getAPNSToken.mockResolvedValue('ABCDEF0123') + const notifications = loadNotifications('ios') + + const token = await notifications.getToken() + + expect(mockMessaging.registerDeviceForRemoteMessages).toHaveBeenCalled() + expect(mockMessaging.getToken).not.toHaveBeenCalled() + expect(token).toEqual({ token: 'abcdef0123', os: 'ios' }) + expect(JSON.parse(mockStorage['@device-token'])).toEqual(token) + }) + + it('falls back to the stored token when APNs has none', async () => { + mockMessaging.isDeviceRegisteredForRemoteMessages.mockReturnValue(true) + mockMessaging.getAPNSToken.mockResolvedValue(null) + mockStorage['@device-token'] = JSON.stringify({ + token: 'stored', + os: 'ios' + }) + const notifications = loadNotifications('ios') + + expect(await notifications.getToken()).toEqual({ + token: 'stored', + os: 'ios' + }) + }) + + it('holds a cold-start tap until navigation is ready, then navigates once', async () => { + const notifications = loadNotifications('ios') + const navigate = jest.fn() + const onForeground = mockNotifee.onForegroundEvent.mock.calls[0][0] + + onForeground( + iosPress({ data: { type: 'Follow', id: 1 }, 'media-url': 'x' }) + ) + notifications.setNavigation({ navigate }) + expect(navigate).not.toHaveBeenCalled() + + await notifications.openInitialNotification() + expect(navigate).toHaveBeenCalledTimes(1) + expect(navigate).toHaveBeenCalledWith({ type: 'Follow', id: 1 }) + expect(mockMessaging.getInitialNotification).not.toHaveBeenCalled() + + // A later remount must not replay it + await notifications.openInitialNotification() + expect(navigate).toHaveBeenCalledTimes(1) + }) + + it('navigates immediately on a tap once ready, ignoring other events', async () => { + const notifications = loadNotifications('ios') + const navigate = jest.fn() + notifications.setNavigation({ navigate }) + await notifications.openInitialNotification() + const onBackground = mockNotifee.onBackgroundEvent.mock.calls[0][0] + + await onBackground({ type: 0, detail: { notification: { data: {} } } }) + expect(navigate).not.toHaveBeenCalled() + + await onBackground(iosPress({ data: { type: 'Repost', id: 2 } })) + expect(navigate).toHaveBeenCalledWith({ type: 'Repost', id: 2 }) + }) + + it('requests permission through firebase and sets the badge', async () => { + const notifications = loadNotifications('ios') + await notifications.requestPermission() + expect(mockMessaging.requestPermission).toHaveBeenCalled() + notifications.setBadgeCount(0) + expect(mockNotifee.setBadgeCount).toHaveBeenCalledWith(0) + }) + }) + + describe('android', () => { + it('registers the FCM token', async () => { + mockMessaging.getToken.mockResolvedValue('fcm:Token') + const notifications = loadNotifications('android') + + expect(await notifications.getToken()).toEqual({ + token: 'fcm:Token', + os: 'android' + }) + expect(mockMessaging.getAPNSToken).not.toHaveBeenCalled() + }) + + it('opens the launch notification with parsed data', async () => { + mockMessaging.getInitialNotification.mockResolvedValue({ + notification: { title: 't', body: 'b' }, + data: { type: 'Follow', id: '12', userIds: '[1,2]' } + }) + const notifications = loadNotifications('android') + const navigate = jest.fn() + notifications.setNavigation({ navigate }) + + await notifications.openInitialNotification() + expect(navigate).toHaveBeenCalledTimes(1) + expect(navigate).toHaveBeenCalledWith({ + type: 'Follow', + id: 12, + userIds: [1, 2] + }) + expect(mockNotifee.onForegroundEvent).not.toHaveBeenCalled() + }) + + it('opens a tap from the background', async () => { + mockMessaging.getInitialNotification.mockResolvedValue(null) + const notifications = loadNotifications('android') + const navigate = jest.fn() + notifications.setNavigation({ navigate }) + await notifications.openInitialNotification() + + const onOpened = mockMessaging.onNotificationOpenedApp.mock.calls[0][1] + onOpened({ data: { type: 'Repost', id: '3' } }) + expect(navigate).toHaveBeenCalledWith({ type: 'Repost', id: 3 }) + }) + + it('does not touch the iOS badge', () => { + const notifications = loadNotifications('android') + notifications.setBadgeCount(0) + expect(mockNotifee.setBadgeCount).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/mobile/src/notifications.ts b/packages/mobile/src/notifications.ts index 036638238f6..1ca54ee4de2 100644 --- a/packages/mobile/src/notifications.ts +++ b/packages/mobile/src/notifications.ts @@ -1,10 +1,22 @@ import { getCurrentAccountQueryKey } from '@audius/common/api' import { MobileOS } from '@audius/common/models' import type { AccountState } from '@audius/common/store' +import notifee, { EventType } from '@notifee/react-native' +import type { Event as NotifeeEvent } from '@notifee/react-native' import AsyncStorage from '@react-native-async-storage/async-storage' +import { + getAPNSToken, + getInitialNotification, + getMessaging, + getToken as getFcmToken, + isDeviceRegisteredForRemoteMessages, + onNotificationOpenedApp, + onTokenRefresh, + registerDeviceForRemoteMessages, + requestPermission +} from '@react-native-firebase/messaging' +import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging' import { Platform } from 'react-native' -import { Notifications } from 'react-native-notifications' -import type { Registered, Notification } from 'react-native-notifications' import { requestNotifications } from 'react-native-permissions' import { track, make } from 'app/services/analytics' @@ -52,7 +64,7 @@ function parseAndroidNotificationData(data: Record): any { } function extractNotificationCampaignIdFromPayload( - payload: Notification['payload'] + payload: Record | undefined ): string | undefined { const target = payload?.data?.data ?? payload?.data ?? payload ?? undefined if (!target || typeof target !== 'object') return undefined @@ -87,29 +99,68 @@ async function reportNotificationCampaignPushOpen( }) } -// Set to true while the push notification service is registering with the os -let isRegistering = false +// A tapped push, normalized across platforms. `payload` matches what +// react-native-notifications used to hand us: the APNs userInfo minus `aps` on +// iOS, the FCM data map on Android. +type OpenedNotification = { + title?: string + body?: string + payload: Record +} + +const fromRemoteMessage = ( + message: FirebaseMessagingTypes.RemoteMessage +): OpenedNotification => ({ + title: message.notification?.title, + body: message.notification?.body, + payload: message.data ?? {} +}) + +// Our iOS pushes go straight to APNs through SNS, not through FCM, so +// @react-native-firebase/messaging ignores taps on them. Notifee reports them +// as PRESS events instead (AppDelegate makes sure it sees them). +const fromNotifeeEvent = ({ + type, + detail +}: NotifeeEvent): OpenedNotification | null => { + if (type !== EventType.PRESS || !detail.notification) return null + return { + title: detail.notification.title, + body: detail.notification.body, + payload: (detail.notification.data ?? {}) as Record + } +} // Singleton class class PushNotifications { - lastId: number token: Token | null navigation: NotificationNavigation | null + // A tap that arrived before navigation was ready (cold start) + private pendingOpen: OpenedNotification | null + private isNavigationReady: boolean - // onNotification is a function passed in that is to be called when a - // notification is to be emitted. constructor() { - this.configure() - this.lastId = 0 this.token = null this.navigation = null + this.pendingOpen = null + this.isNavigationReady = false + this.configure() } setNavigation = (navigation: NotificationNavigation) => { this.navigation = navigation } - onNotification = (notification: Notification) => { + private handleOpened = (notification: OpenedNotification | null) => { + if (!notification) return + if (!this.isNavigationReady) { + this.pendingOpen = notification + return + } + this.onNotification(notification) + } + + onNotification = (notification: OpenedNotification) => { console.info(`Received notification ${JSON.stringify(notification)}`) const { title, body, payload } = notification const notificationCampaignId = @@ -138,19 +189,43 @@ class PushNotifications { this.navigation?.navigate(data) } - // Method used to open the push notification that the user pressed while the app was closed + // Called once navigation is ready. Opens the push the user tapped to launch + // the app, if any. openInitialNotification = async () => { - const notification = await Notifications.getInitialNotification() + this.isNavigationReady = true + if (Platform.OS === MobileOS.ANDROID) { + const message = await getInitialNotification(getMessaging()) + if (message) { + this.pendingOpen = fromRemoteMessage(message) + } + } + const notification = this.pendingOpen + this.pendingOpen = null if (notification) { this.onNotification(notification) } } - async onRegister(event: Registered) { - const token = { token: event.deviceToken, os: Platform.OS } + private async persistToken(deviceToken: string) { + const token = { token: deviceToken, os: Platform.OS } this.token = token await AsyncStorage.setItem(DEVICE_TOKEN, JSON.stringify(token)) - isRegistering = false + return token + } + + // identity-service registers iOS tokens with an SNS APNs platform app, so iOS + // must send the raw APNs token, not an FCM token. Lowercase hex matches what + // react-native-notifications sent, so existing rows and endpoints are reused. + private async fetchDeviceToken() { + if (Platform.OS === MobileOS.IOS) { + const messaging = getMessaging() + if (!isDeviceRegisteredForRemoteMessages(messaging)) { + await registerDeviceForRemoteMessages(messaging) + } + const apnsToken = await getAPNSToken(messaging) + return apnsToken ? apnsToken.toLowerCase() : null + } + return await getFcmToken(getMessaging()) } deregister() { @@ -158,10 +233,22 @@ class PushNotifications { } async configure() { - Notifications.events().registerRemoteNotificationsRegistered( - this.onRegister - ) - Notifications.events().registerNotificationOpened(this.onNotification) + if (Platform.OS === MobileOS.IOS) { + notifee.onForegroundEvent((event) => + this.handleOpened(fromNotifeeEvent(event)) + ) + notifee.onBackgroundEvent(async (event) => + this.handleOpened(fromNotifeeEvent(event)) + ) + } else { + const messaging = getMessaging() + onNotificationOpenedApp(messaging, (message) => + this.handleOpened(fromRemoteMessage(message)) + ) + onTokenRefresh(messaging, (token) => { + this.persistToken(token).catch(() => {}) + }) + } try { const token = await AsyncStorage.getItem(DEVICE_TOKEN) @@ -175,40 +262,30 @@ class PushNotifications { } } - async hasPermission(): Promise { - return await Notifications.isRegisteredForRemoteNotifications() - } - async requestPermission() { - isRegistering = true - if (Platform.OS === MobileOS.ANDROID) { // Android 13+ needs POST_NOTIFICATIONS. Use requestNotifications — PERMISSIONS.ANDROID // does not expose POST_NOTIFICATIONS in react-native-permissions v5, so request(undefined) crashed native code. await requestNotifications() + } else { + await requestPermission(getMessaging()) } - - Notifications.registerRemoteNotifications() - } - - cancelNotif() { - Notifications.cancelLocalNotification(this.lastId) - } - - cancelAll() { - Notifications.ios.cancelAllLocalNotifications() } setBadgeCount(count: number) { - Notifications.ios.setBadgeCount(count) + if (Platform.OS === MobileOS.IOS) { + notifee.setBadgeCount(count) + } } async getToken() { - // Wait until the device token and OS are persisted to async storage - // isRegistering modified as global - // eslint-disable-next-line no-unmodified-loop-condition - while (isRegistering) { - await new Promise((resolve) => setTimeout(resolve, 100)) + try { + const deviceToken = await this.fetchDeviceToken() + if (deviceToken) { + return await this.persistToken(deviceToken) + } + } catch (e) { + console.error('Failed to fetch push token', e) } const token = await AsyncStorage.getItem(DEVICE_TOKEN) if (token) { diff --git a/packages/mobile/src/screens/audio-screen/AudioScreen.tsx b/packages/mobile/src/screens/audio-screen/AudioScreen.tsx index 7a8b6cbdc95..95171a811e6 100644 --- a/packages/mobile/src/screens/audio-screen/AudioScreen.tsx +++ b/packages/mobile/src/screens/audio-screen/AudioScreen.tsx @@ -11,10 +11,10 @@ import { AUDIO } from '@audius/fixed-decimal' import { useFocusEffect } from '@react-navigation/native' import { Image, Linking } from 'react-native' import { TouchableOpacity } from 'react-native-gesture-handler' -import LinearGradient from 'react-native-linear-gradient' import { useDispatch, useSelector } from 'react-redux' import { + LinearGradient, Button, Flex, IconCrown, diff --git a/packages/mobile/src/screens/coin-details-screen/EditCoinDetailsScreen.tsx b/packages/mobile/src/screens/coin-details-screen/EditCoinDetailsScreen.tsx index 77ef64dd304..d8d56866260 100644 --- a/packages/mobile/src/screens/coin-details-screen/EditCoinDetailsScreen.tsx +++ b/packages/mobile/src/screens/coin-details-screen/EditCoinDetailsScreen.tsx @@ -19,9 +19,9 @@ import { useRoute, useNavigation } from '@react-navigation/native' import { useFormikContext, Formik } from 'formik' import { Image as RNImage, View, StyleSheet } from 'react-native' import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' -import LinearGradient from 'react-native-linear-gradient' import { + LinearGradient, IconInstagram, IconLink, IconPlus, diff --git a/packages/mobile/src/screens/coin-details-screen/components/CoinInfoCard.tsx b/packages/mobile/src/screens/coin-details-screen/components/CoinInfoCard.tsx index 23c1aa84e46..f487b53c9ec 100644 --- a/packages/mobile/src/screens/coin-details-screen/components/CoinInfoCard.tsx +++ b/packages/mobile/src/screens/coin-details-screen/components/CoinInfoCard.tsx @@ -7,9 +7,9 @@ import { WidthSizes } from '@audius/common/models' import { shortenSPLAddress } from '@audius/common/utils' import Clipboard from '@react-native-clipboard/clipboard' import { Image, Linking, StyleSheet, View } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import { + LinearGradient, Divider, Flex, IconCopy, diff --git a/packages/mobile/src/screens/coin-details-screen/components/GraduationProgressBar.tsx b/packages/mobile/src/screens/coin-details-screen/components/GraduationProgressBar.tsx index 83d055a63d0..074dfe5de07 100644 --- a/packages/mobile/src/screens/coin-details-screen/components/GraduationProgressBar.tsx +++ b/packages/mobile/src/screens/coin-details-screen/components/GraduationProgressBar.tsx @@ -1,6 +1,4 @@ -import LinearGradient from 'react-native-linear-gradient' - -import { Box, spacing, useTheme } from '@audius/harmony-native' +import { LinearGradient, Box, spacing, useTheme } from '@audius/harmony-native' interface GraduationProgressBarProps { value: number diff --git a/packages/mobile/src/screens/explore-screen/components/ColorTile.tsx b/packages/mobile/src/screens/explore-screen/components/ColorTile.tsx index 7a62aa68c1f..681309bdd88 100644 --- a/packages/mobile/src/screens/explore-screen/components/ColorTile.tsx +++ b/packages/mobile/src/screens/explore-screen/components/ColorTile.tsx @@ -7,9 +7,9 @@ import type { ViewStyle } from 'react-native' import { Animated, Image, View } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import type { SvgProps } from 'react-native-svg' +import { LinearGradient } from '@audius/harmony-native' import IconAudioRewardsPill from 'app/assets/images/iconAudioRewardsPill.svg' import { Pressable } from 'app/components/core' import Text from 'app/components/text' @@ -142,7 +142,6 @@ export const ColorTile = ({ > diff --git a/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx b/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx index 9b8140b9f77..3a0c01eec13 100644 --- a/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx +++ b/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx @@ -5,10 +5,9 @@ import { useRemoteVar } from '@audius/common/hooks' import { StringKeys } from '@audius/common/services' import { tokenDashboardPageActions } from '@audius/common/store' import { useFocusEffect } from '@react-navigation/native' -import LinearGradient from 'react-native-linear-gradient' import { useDispatch } from 'react-redux' -import { IconCrown, Flex, Text } from '@audius/harmony-native' +import { LinearGradient, IconCrown, Flex, Text } from '@audius/harmony-native' import { ScrollView, Screen, Tile, ScreenContent } from 'app/components/core' import { useToast } from 'app/hooks/useToast' import { makeStyles } from 'app/styles' diff --git a/packages/mobile/src/screens/rewards-screen/TiersTile.tsx b/packages/mobile/src/screens/rewards-screen/TiersTile.tsx index ebca2560060..627a3de67f5 100644 --- a/packages/mobile/src/screens/rewards-screen/TiersTile.tsx +++ b/packages/mobile/src/screens/rewards-screen/TiersTile.tsx @@ -13,9 +13,9 @@ import { badgeTiers, getTierAndNumberForBalance } from '@audius/common/store' import type { Nullable } from '@audius/common/utils' import { formatNumberCommas } from '@audius/common/utils' import { Linking } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import { + LinearGradient, Button, Flex, IconDiscord, diff --git a/packages/mobile/src/shims/react-native-linear-gradient.ts b/packages/mobile/src/shims/react-native-linear-gradient.ts new file mode 100644 index 00000000000..6511eae8bf7 --- /dev/null +++ b/packages/mobile/src/shims/react-native-linear-gradient.ts @@ -0,0 +1,6 @@ +// Metro resolves `react-native-linear-gradient` here so dependencies that +// still import it (react-native-gifted-charts) render our svg gradient. +import { LinearGradient } from 'app/harmony-native/components/LinearGradient/LinearGradient' + +export { LinearGradient } +export default LinearGradient diff --git a/packages/mobile/src/store/offline-downloads/sagas/migrateOfflineDataPathSaga.ts b/packages/mobile/src/store/offline-downloads/sagas/migrateOfflineDataPathSaga.ts index fb72c845b97..ecbbfcee866 100644 --- a/packages/mobile/src/store/offline-downloads/sagas/migrateOfflineDataPathSaga.ts +++ b/packages/mobile/src/store/offline-downloads/sagas/migrateOfflineDataPathSaga.ts @@ -1,8 +1,8 @@ import path from 'path' +import * as RNFS from '@dr.pogodin/react-native-fs' import { difference } from 'lodash' import ReactNativeBlobUtil from 'react-native-blob-util' -import RNFS from 'react-native-fs' import { call, put, select } from 'typed-redux-saga' import { downloadsRoot } from 'app/services/offline-downloader'