diff --git a/package-lock.json b/package-lock.json index 6ae1105..6d18b47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,12 @@ "version": "0.0.1", "hasInstallScript": true, "dependencies": { - "@react-native-async-storage/async-storage": "^1.19.0", + "@bankify/redux-persist-realm": "^0.1.3", "@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/native": "^6.1.7", "@react-navigation/native-stack": "^6.9.13", "@realm/react": "^0.5.1", + "@reduxjs/toolkit": "^1.9.5", "@shopify/flash-list": "^1.4.3", "react": "18.2.0", "react-native": "0.72.1", @@ -27,7 +28,9 @@ "react-native-scoped-storage": "^1.9.3", "react-native-screens": "^3.22.1", "react-native-vector-icons": "^9.2.0", - "realm": "^11.10.1" + "react-redux": "^8.1.1", + "realm": "^11.10.1", + "redux-persist": "^6.0.0" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -2057,6 +2060,15 @@ "node": ">=6.9.0" } }, + "node_modules/@bankify/redux-persist-realm": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@bankify/redux-persist-realm/-/redux-persist-realm-0.1.3.tgz", + "integrity": "sha512-qVxkB6pVNMwJWa6K4HRId6bZbWx7PUyUYY4YPkS98Uobw+P3ujQkF2kBcdHWHeT71AsGvSdK/LVKRqvae74Jww==", + "peerDependencies": { + "realm": "*", + "redux-persist": "*" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -3022,17 +3034,6 @@ "node": ">= 8" } }, - "node_modules/@react-native-async-storage/async-storage": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.19.0.tgz", - "integrity": "sha512-xOFkz/FaQctD6yNJDur+WnHdSTigOs3pTz6HmfC8X8PYwcnnN3R9UxuWiwsfK8vvT2WioAxUkQt3lB7GySNA2w==", - "dependencies": { - "merge-options": "^3.0.4" - }, - "peerDependencies": { - "react-native": "^0.0.0-0 || 0.60 - 0.72 || 1000.0.0" - } - }, "node_modules/@react-native-community/cli": { "version": "11.3.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-11.3.3.tgz", @@ -4833,6 +4834,29 @@ "realm": "^12.0.0-browser || ^12.0.0 || ^12.0.0-rc || ^11.0.0-rc || ^11.0.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@shopify/flash-list": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.4.3.tgz", @@ -4952,6 +4976,15 @@ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz", "integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -5031,14 +5064,12 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { "version": "18.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz", "integrity": "sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -5076,8 +5107,7 @@ "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "node_modules/@types/semver": { "version": "7.5.0", @@ -5090,6 +5120,11 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -6704,8 +6739,7 @@ "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/dashdash": { "version": "1.14.1", @@ -8854,6 +8888,15 @@ "node": ">=14.0.0" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -9191,14 +9234,6 @@ "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -11600,17 +11635,6 @@ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, - "node_modules/merge-options": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", - "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", - "dependencies": { - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -14263,6 +14287,49 @@ "node": ">=12" } }, + "node_modules/react-redux": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz", + "integrity": "sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/react-refresh": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", @@ -14510,6 +14577,30 @@ "react-native": ">= 0.30.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "peerDependencies": { + "redux": ">4.0.0" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -14676,6 +14767,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -17721,6 +17817,12 @@ "to-fast-properties": "^2.0.0" } }, + "@bankify/redux-persist-realm": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@bankify/redux-persist-realm/-/redux-persist-realm-0.1.3.tgz", + "integrity": "sha512-qVxkB6pVNMwJWa6K4HRId6bZbWx7PUyUYY4YPkS98Uobw+P3ujQkF2kBcdHWHeT71AsGvSdK/LVKRqvae74Jww==", + "requires": {} + }, "@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -18453,14 +18555,6 @@ "fastq": "^1.6.0" } }, - "@react-native-async-storage/async-storage": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.19.0.tgz", - "integrity": "sha512-xOFkz/FaQctD6yNJDur+WnHdSTigOs3pTz6HmfC8X8PYwcnnN3R9UxuWiwsfK8vvT2WioAxUkQt3lB7GySNA2w==", - "requires": { - "merge-options": "^3.0.4" - } - }, "@react-native-community/cli": { "version": "11.3.3", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-11.3.3.tgz", @@ -19829,6 +19923,17 @@ "react-native": ">=0.68" } }, + "@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "requires": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + } + }, "@shopify/flash-list": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.4.3.tgz", @@ -19945,6 +20050,15 @@ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz", "integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==" }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -20023,14 +20137,12 @@ "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "@types/react": { "version": "18.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.14.tgz", "integrity": "sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==", - "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -20068,8 +20180,7 @@ "@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "@types/semver": { "version": "7.5.0", @@ -20082,6 +20193,11 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -21279,8 +21395,7 @@ "csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "dashdash": { "version": "1.14.1", @@ -22845,6 +22960,11 @@ "queue": "6.0.2" } }, + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, "immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -23076,11 +23196,6 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" - }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -24882,14 +24997,6 @@ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, - "merge-options": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", - "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", - "requires": { - "is-plain-obj": "^2.1.0" - } - }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -26868,6 +26975,26 @@ "yargs": "^16.1.1" } }, + "react-redux": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz", + "integrity": "sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "react-refresh": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", @@ -27060,6 +27187,26 @@ "ts-object-utils": "0.0.5" } }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "requires": {} + }, + "redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "requires": {} + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -27196,6 +27343,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", diff --git a/package.json b/package.json index 4c6d31f..057149e 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ "clean": "cd android && ./gradlew clean && cd .." }, "dependencies": { - "@react-native-async-storage/async-storage": "^1.19.0", + "@bankify/redux-persist-realm": "^0.1.3", "@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/native": "^6.1.7", "@react-navigation/native-stack": "^6.9.13", "@realm/react": "^0.5.1", + "@reduxjs/toolkit": "^1.9.5", "@shopify/flash-list": "^1.4.3", "react": "18.2.0", "react-native": "0.72.1", @@ -32,7 +33,9 @@ "react-native-scoped-storage": "^1.9.3", "react-native-screens": "^3.22.1", "react-native-vector-icons": "^9.2.0", - "realm": "^11.10.1" + "react-redux": "^8.1.1", + "realm": "^11.10.1", + "redux-persist": "^6.0.0" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/src/app.tsx b/src/app.tsx index 5972901..82177ac 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,32 +1,77 @@ -import React from 'react'; -import { StatusBar, useColorScheme } from 'react-native'; +import React, { useEffect, useState } from 'react'; +import { AppState, StatusBar, useColorScheme } from 'react-native'; import { PaperProvider } from 'react-native-paper'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { RealmProvider } from '@realm/react'; +import { Provider } from 'react-redux'; +import { PersistGate } from 'redux-persist/integration/react'; +import { openDocumentTree } from 'react-native-scoped-storage'; +import type {} from 'redux-thunk/extend-redux'; import { lightTheme, darkTheme } from './theme'; import { Meme, Tag } from './database'; import NavigationContainer from './navigation'; -import { SettingsProvider } from './contexts'; +import { store, persistor, updateStorageUri, validateSettings } from './redux'; +import { LoadingView } from './components'; +import { Welcome } from './screens'; +import { noOp } from './constants'; const App = () => { + const [showWelcome, setShowWelcome] = useState(false); const colorScheme = useColorScheme(); const isDarkMode = colorScheme === 'dark'; const theme = isDarkMode ? darkTheme : lightTheme; + const onBeforeLift = async () => { + await store.dispatch(validateSettings()); + const { settings } = store.getState(); + if (!settings.storageUri) { + setShowWelcome(true); + } + }; + + useEffect(() => { + const subscription = AppState.addEventListener('change', async state => { + if (state !== 'active') return; + + await store.dispatch(validateSettings()); + const { settings } = store.getState(); + if (!settings.storageUri) { + setShowWelcome(true); + } + }); + return () => subscription.remove(); + }, []); + return ( - - - - - - - - - - + + } + persistor={persistor} + onBeforeLift={onBeforeLift}> + + + + + {showWelcome ? ( + { + const uri = await openDocumentTree(true).catch(noOp); + if (!uri) return; + await store.dispatch(updateStorageUri(uri.uri)); + setShowWelcome(false); + }} + /> + ) : ( + + )} + + + + + ); }; diff --git a/src/contexts/index.ts b/src/contexts/index.ts deleted file mode 100644 index a540f75..0000000 --- a/src/contexts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SettingsProvider, useStorageUri, useNoMedia } from './settings'; diff --git a/src/contexts/settings.tsx b/src/contexts/settings.tsx deleted file mode 100644 index dbe6c8c..0000000 --- a/src/contexts/settings.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React, { - createContext, - useContext, - useState, - useEffect, - ReactNode, -} from 'react'; -import { AppState } from 'react-native'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { AndroidScoped, FileSystem } from 'react-native-file-access'; -import { - getPersistedUriPermissions, - openDocumentTree, - createFile, - deleteFile, -} from 'react-native-scoped-storage'; -import { LoadingView } from '../components'; -import { Welcome } from '../screens'; -import { clearPermissions, isPermissionForPath } from '../utilities'; - -interface SettingsContextType { - storageUri: string; - noMedia: boolean; - setStorageUri: (newStorageUri: string) => Promise; - setNoMedia: (newNoMedia: boolean) => Promise; -} - -const SettingsContext = createContext( - undefined, -); - -const SettingsProvider = ({ children }: { children: ReactNode }) => { - const [storageUri, setStorageUri] = useState(''); - const [noMedia, setNoMedia] = useState(false); - - const [hasLoaded, setHasLoaded] = useState(false); - - const updateStorageUri = async (newStorageUri: string): Promise => { - setStorageUri(newStorageUri); - - void clearPermissions([newStorageUri]); - void AsyncStorage.setItem('storageUri', newStorageUri); - }; - - const updateNoMedia = async (newNoMedia: boolean): Promise => { - setNoMedia(newNoMedia); - - const noMediaExists = await FileSystem.exists( - AndroidScoped.appendPath(storageUri, '.nomedia'), - ); - - if (newNoMedia && !noMediaExists) { - await createFile(storageUri, '.nomedia', 'text/x-unknown'); - } else if (!newNoMedia && noMediaExists) { - await deleteFile(AndroidScoped.appendPath(storageUri, '.nomedia')); - } - - void AsyncStorage.setItem('noMedia', newNoMedia.toString()); - }; - - useEffect(() => { - const loadSettings = async () => { - let storageUriValue = (await AsyncStorage.getItem('storageUri')) ?? ''; - let noMediaValue = (await AsyncStorage.getItem('noMedia')) === 'true'; - - if (storageUriValue !== '') { - const permissions = await getPersistedUriPermissions(); - if ( - !permissions.some(permission => - isPermissionForPath(permission, storageUriValue), - ) - ) { - storageUriValue = ''; - } - - try { - const exists = await FileSystem.exists(storageUriValue); - if (!exists) { - throw new Error('Storage URI does not exist'); - } - - const isDirectory = await FileSystem.isDir(storageUriValue); - if (!isDirectory) { - throw new Error('Storage URI is not a directory'); - } - } catch { - storageUriValue = ''; - } - - noMediaValue = await FileSystem.exists( - AndroidScoped.appendPath(storageUriValue, '.nomedia'), - ); - } - - setStorageUri(storageUriValue); - setNoMedia(noMediaValue); - setHasLoaded(true); - }; - - const subscription = AppState.addEventListener('change', () => { - if (AppState.currentState === 'active') void loadSettings(); - }); - - return () => subscription.remove(); - }, []); - - return ( - - {hasLoaded ? ( - storageUri === '' ? ( - { - const { uri } = await openDocumentTree(true); - await updateStorageUri(uri); - }} - /> - ) : ( - children - ) - ) : ( - - )} - - ); -}; - -const useStorageUri = (): [ - string, - (newStorageUri: string) => Promise, -] => { - const context = useContext(SettingsContext); - if (!context) { - throw new Error('useStorageUri must be used within a SettingsProvider'); - } - return [context.storageUri, context.setStorageUri]; -}; - -const useNoMedia = (): [boolean, (newNoMedia: boolean) => Promise] => { - const context = useContext(SettingsContext); - if (!context) { - throw new Error('useNoMedia must be used within a SettingsProvider'); - } - return [context.noMedia, context.setNoMedia]; -}; - -export { SettingsProvider, useStorageUri, useNoMedia }; diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..2843927 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,5 @@ +declare module '@bankify/redux-persist-realm' { + function createRealmPersistStorage(options?: { + path: string; + }): import('redux-persist').Storage; +} diff --git a/src/redux/index.ts b/src/redux/index.ts new file mode 100644 index 0000000..c945535 --- /dev/null +++ b/src/redux/index.ts @@ -0,0 +1,48 @@ +import { configureStore, combineReducers } from '@reduxjs/toolkit'; +import { + persistReducer, + persistStore, + FLUSH, + REHYDRATE, + PAUSE, + PERSIST, + PURGE, + REGISTER, +} from 'redux-persist'; +import { createRealmPersistStorage } from '@bankify/redux-persist-realm'; +import settingsReducer from './settings'; + +const rootReducer = combineReducers({ + settings: settingsReducer, +}); + +interface RootState { + settings: ReturnType; +} + +const persistConfig = { + key: 'root', + storage: createRealmPersistStorage({ path: 'redux.realm' }), +}; + +const persistedReducer = persistReducer(persistConfig, rootReducer); + +const store = configureStore({ + reducer: persistedReducer, + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + }), +}); + +const persistor = persistStore(store); + +export { type RootState, store, persistor }; +export { + type SettingsState, + updateStorageUri, + updateNoMedia, + validateSettings, +} from './settings'; diff --git a/src/redux/settings.ts b/src/redux/settings.ts new file mode 100644 index 0000000..f5e5deb --- /dev/null +++ b/src/redux/settings.ts @@ -0,0 +1,114 @@ +import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; +import { + createFile, + deleteFile, + getPersistedUriPermissions, +} from 'react-native-scoped-storage'; +import { FileSystem, AndroidScoped } from 'react-native-file-access'; +import { clearPermissions, isPermissionForPath } from '../utilities'; +import { RootState } from '.'; + +interface SettingsState { + storageUri: string | undefined; + noMedia: boolean; +} + +const initialState: SettingsState = { + storageUri: undefined, + noMedia: false, +}; + +const settingsSlice = createSlice({ + name: 'settings', + initialState, + reducers: { + setStorageUri: (state, action: PayloadAction) => { + state.storageUri = action.payload; + }, + setNoMedia: (state, action: PayloadAction) => { + state.noMedia = action.payload; + }, + }, +}); + +const { setStorageUri, setNoMedia } = settingsSlice.actions; + +const updateStorageUri = createAsyncThunk( + 'settings/updateStorageUri', + async (newStorageUri: string, { dispatch }) => { + await clearPermissions([newStorageUri]); + dispatch(setStorageUri(newStorageUri)); + }, +); + +const updateNoMedia = createAsyncThunk( + 'settings/updateNoMedia', + async (newNoMedia: boolean, { dispatch, getState }) => { + const state = getState() as RootState; + const { storageUri } = state.settings; + if (!storageUri) return; + + const noMediaExists = await FileSystem.exists( + AndroidScoped.appendPath(storageUri, '.nomedia'), + ); + if (newNoMedia && !noMediaExists) { + await createFile(storageUri, '.nomedia', 'text/x-unknown'); + } else if (!newNoMedia && noMediaExists) { + await deleteFile(AndroidScoped.appendPath(storageUri, '.nomedia')); + } + dispatch(setNoMedia(newNoMedia)); + }, +); + +const validateSettings = createAsyncThunk( + 'settings/validateSettings', + // eslint-disable-next-line @typescript-eslint/naming-convention + async (_, { dispatch, getState }) => { + const state = getState() as RootState; + const { storageUri, noMedia } = state.settings; + + if (!storageUri) { + return; + } + + const permissions = await getPersistedUriPermissions(); + if ( + !permissions.some(permission => + isPermissionForPath(permission, storageUri), + ) + ) { + dispatch(setStorageUri()); + return; + } + + try { + const exists = await FileSystem.exists(storageUri); + if (!exists) { + throw new Error('Storage URI does not exist'); + } + + const isDirectory = await FileSystem.isDir(storageUri); + if (!isDirectory) { + throw new Error('Storage URI is not a directory'); + } + } catch { + dispatch(setStorageUri()); + return; + } + + const isNoMedia = await FileSystem.exists( + AndroidScoped.appendPath(storageUri, '.nomedia'), + ); + if (noMedia !== isNoMedia) { + dispatch(setNoMedia(isNoMedia)); + } + }, +); + +export { + type SettingsState, + updateStorageUri, + updateNoMedia, + validateSettings, +}; +export default settingsSlice.reducer; diff --git a/src/screens/settings.tsx b/src/screens/settings.tsx index 6a1931b..b92ad32 100644 --- a/src/screens/settings.tsx +++ b/src/screens/settings.tsx @@ -3,20 +3,22 @@ import { View } from 'react-native'; import { Button, List, Snackbar, Switch, Text } from 'react-native-paper'; import { useRealm } from '@realm/react'; import { openDocumentTree } from 'react-native-scoped-storage'; +import { useDispatch, useSelector } from 'react-redux'; import { PaddedView } from '../components'; import styles from '../styles'; import { Meme } from '../database'; -import { useStorageUri, useNoMedia } from '../contexts'; +import { RootState, updateNoMedia, updateStorageUri } from '../redux'; +import type {} from 'redux-thunk/extend-redux'; const SettingsScreen = () => { const [optimizingDatabase, setOptimizingDatabase] = useState(false); + const noMedia = useSelector((state: RootState) => state.settings.noMedia); + const dispatch = useDispatch(); + const [snackbarVisible, setSnackbarVisible] = useState(false); const [snackbarMessage, setSnackbarMessage] = useState(''); - const [, setStorageUri] = useStorageUri(); - const [noMedia, setNoMedia] = useNoMedia(); - const realm = useRealm(); const optimizeDatabase = () => { @@ -62,7 +64,7 @@ const SettingsScreen = () => { style={styles.marginBottom} onPress={async () => { const { uri } = await openDocumentTree(true); - void setStorageUri(uri); + void dispatch(updateStorageUri(uri)); }}> Change External Storage Path @@ -76,7 +78,7 @@ const SettingsScreen = () => { { - void setNoMedia(value); + void dispatch(updateNoMedia(value)); }} /> diff --git a/src/screens/welcome.tsx b/src/screens/welcome.tsx index 6b3739f..96e1706 100644 --- a/src/screens/welcome.tsx +++ b/src/screens/welcome.tsx @@ -3,7 +3,9 @@ import { Button, Text } from 'react-native-paper'; import { PaddedView } from '../components'; import styles from '../styles'; -const Welcome = (properties: { selectStorageLocation: () => void }) => { +const Welcome = (properties: { + selectStorageLocation: () => Promise; +}) => { return ( void }) => { style={[styles.bigMarginBottom, styles.centerText]}> Welcome to Terminally Online! -