diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0c46ef6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.tabSize": 2, + "prettier.jsxSingleQuote": true, + "javascript.preferences.quoteStyle": "single", + "prettier.singleQuote": true +} diff --git a/assets/avatar.png b/assets/avatar.png new file mode 100644 index 0000000..b89c456 Binary files /dev/null and b/assets/avatar.png differ diff --git a/assets/blur.jpg b/assets/blur.jpg new file mode 100644 index 0000000..e79c934 Binary files /dev/null and b/assets/blur.jpg differ diff --git a/assets/empty_box.png b/assets/empty_box.png new file mode 100644 index 0000000..17abdf2 Binary files /dev/null and b/assets/empty_box.png differ diff --git a/assets/familyView.png b/assets/familyView.png new file mode 100644 index 0000000..c26d65c Binary files /dev/null and b/assets/familyView.png differ diff --git a/assets/loginBg.png b/assets/loginBg.png new file mode 100644 index 0000000..c07f2aa Binary files /dev/null and b/assets/loginBg.png differ diff --git a/assets/loginIcon.png b/assets/loginIcon.png new file mode 100644 index 0000000..785ba3e Binary files /dev/null and b/assets/loginIcon.png differ diff --git a/assets/loginPage.png b/assets/loginPage.png new file mode 100644 index 0000000..c23d242 Binary files /dev/null and b/assets/loginPage.png differ diff --git a/assets/openBankingHead.png b/assets/openBankingHead.png new file mode 100644 index 0000000..4b0b627 Binary files /dev/null and b/assets/openBankingHead.png differ diff --git a/assets/personalDefaultPage.png b/assets/personalDefaultPage.png new file mode 100644 index 0000000..80315f4 Binary files /dev/null and b/assets/personalDefaultPage.png differ diff --git a/assets/profile.jpg b/assets/profile.jpg new file mode 100644 index 0000000..2db060d Binary files /dev/null and b/assets/profile.jpg differ diff --git a/babel.config.js b/babel.config.js index f6d217d..58869ad 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,6 +1,9 @@ module.exports = (api) => { api.cache(true); return { - presets: ['babel-preset-expo'], + presets: [ + 'babel-preset-expo', + 'module:react-native-dotenv', + ], }; }; diff --git a/package-lock.json b/package-lock.json index 989d74f..9106219 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2850,6 +2850,14 @@ "requires": { "react-is": "^16.7.0" } + }, + "react-native-screens": { + "version": "1.0.0-alpha.23", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-1.0.0-alpha.23.tgz", + "integrity": "sha512-tOxHGQUN83MTmQB4ghoQkibqOdGiX4JQEmeyEv96MKWO/x8T2PJv84ECUos9hD3blPRQwVwSpAid1PPPhrVEaw==", + "requires": { + "debounce": "^1.2.0" + } } } }, @@ -3634,6 +3642,21 @@ "slash": "^2.0.0" } }, + "babel-plugin-dotenv": { + "version": "0.1.1", + "resolved": "https://registry.npm.taobao.org/babel-plugin-dotenv/download/babel-plugin-dotenv-0.1.1.tgz", + "integrity": "sha1-nI+upnp8A0/n6UCZGHqy51c0ALw=", + "requires": { + "dotenv": "^2.0.0" + }, + "dependencies": { + "dotenv": { + "version": "2.0.0", + "resolved": "https://registry.npm.taobao.org/dotenv/download/dotenv-2.0.0.tgz?cache=0&sync_timestamp=1571190782798&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdotenv%2Fdownload%2Fdotenv-2.0.0.tgz", + "integrity": "sha1-vXWcNXqqcDZeAclrewvsCKbg2Uk=" + } + } + }, "babel-plugin-dynamic-import-node": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", @@ -3880,11 +3903,25 @@ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "blueimp-md5": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.12.0.tgz", "integrity": "sha512-zo+HIdIhzojv6F1siQPqPFROyVy7C50KzHv/k/Iz+BtvtVzSHXiMXOpq2wCfNkeBqdCv+V8XOV96tsEt2W/3rQ==" }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, "bplist-creator": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.8.tgz", @@ -4428,6 +4465,38 @@ } } }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-tree": { + "version": "1.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz", + "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==", + "requires": { + "mdn-data": "2.0.6", + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "css-what": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz", + "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==" + }, "cssom": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", @@ -4605,6 +4674,17 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, + "deprecated-react-native-listview": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/deprecated-react-native-listview/-/deprecated-react-native-listview-0.0.5.tgz", + "integrity": "sha512-Cy7nDdd+KU+nR3tY1BSMuoZpsYC6OVSZyAiUSTDBop2lIgzCseDx7XI57x6h+NXer/8aor2yiQDQfeFcmBMwgQ==", + "requires": { + "create-react-class": "*", + "fbjs": "*", + "invariant": "*", + "react-clone-referenced-element": "*" + } + }, "destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", @@ -4631,11 +4711,32 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" + } + } + }, "dom-walk": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=" }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -4645,6 +4746,20 @@ "webidl-conversions": "^4.0.2" } }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -4692,6 +4807,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" + }, "envinfo": { "version": "5.12.1", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-5.12.1.tgz", @@ -5670,6 +5790,12 @@ "flat-cache": "^2.0.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -5852,13 +5978,14 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", + "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" + "node-pre-gyp": "*" }, "dependencies": { "abbrev": { @@ -5900,7 +6027,7 @@ } }, "chownr": { - "version": "1.1.1", + "version": "1.1.3", "bundled": true, "optional": true }, @@ -5925,7 +6052,7 @@ "optional": true }, "debug": { - "version": "4.1.1", + "version": "3.2.6", "bundled": true, "optional": true, "requires": { @@ -5948,11 +6075,11 @@ "optional": true }, "fs-minipass": { - "version": "1.2.5", + "version": "1.2.7", "bundled": true, "optional": true, "requires": { - "minipass": "^2.2.1" + "minipass": "^2.6.0" } }, "fs.realpath": { @@ -5976,7 +6103,7 @@ } }, "glob": { - "version": "7.1.3", + "version": "7.1.6", "bundled": true, "optional": true, "requires": { @@ -6002,7 +6129,7 @@ } }, "ignore-walk": { - "version": "3.0.1", + "version": "3.0.3", "bundled": true, "optional": true, "requires": { @@ -6019,7 +6146,7 @@ } }, "inherits": { - "version": "2.0.3", + "version": "2.0.4", "bundled": true, "optional": true }, @@ -6055,7 +6182,7 @@ "optional": true }, "minipass": { - "version": "2.3.5", + "version": "2.9.0", "bundled": true, "optional": true, "requires": { @@ -6064,11 +6191,11 @@ } }, "minizlib": { - "version": "1.2.1", + "version": "1.3.3", "bundled": true, "optional": true, "requires": { - "minipass": "^2.2.1" + "minipass": "^2.9.0" } }, "mkdirp": { @@ -6080,22 +6207,22 @@ } }, "ms": { - "version": "2.1.1", + "version": "2.1.2", "bundled": true, "optional": true }, "needle": { - "version": "2.3.0", + "version": "2.4.0", "bundled": true, "optional": true, "requires": { - "debug": "^4.1.0", + "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" } }, "node-pre-gyp": { - "version": "0.12.0", + "version": "0.14.0", "bundled": true, "optional": true, "requires": { @@ -6108,7 +6235,7 @@ "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", - "tar": "^4" + "tar": "^4.4.2" } }, "nopt": { @@ -6121,12 +6248,20 @@ } }, "npm-bundled": { - "version": "1.0.6", + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", "bundled": true, "optional": true }, "npm-packlist": { - "version": "1.4.1", + "version": "1.4.7", "bundled": true, "optional": true, "requires": { @@ -6188,7 +6323,7 @@ "optional": true }, "process-nextick-args": { - "version": "2.0.0", + "version": "2.0.1", "bundled": true, "optional": true }, @@ -6225,7 +6360,7 @@ } }, "rimraf": { - "version": "2.6.3", + "version": "2.7.1", "bundled": true, "optional": true, "requires": { @@ -6248,7 +6383,7 @@ "optional": true }, "semver": { - "version": "5.7.0", + "version": "5.7.1", "bundled": true, "optional": true }, @@ -6294,17 +6429,17 @@ "optional": true }, "tar": { - "version": "4.4.8", + "version": "4.4.13", "bundled": true, "optional": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" + "yallist": "^3.0.3" } }, "util-deprecate": { @@ -6326,7 +6461,7 @@ "optional": true }, "yallist": { - "version": "3.0.3", + "version": "3.1.1", "bundled": true, "optional": true } @@ -9728,6 +9863,11 @@ "buffer-alloc": "^1.1.0" } }, + "mdn-data": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz", + "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==" + }, "mem": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", @@ -10503,6 +10643,14 @@ "gauge": "~1.2.5" } }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, "nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -10953,6 +11101,11 @@ "pify": "^2.0.0" } }, + "paths-js": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/paths-js/-/paths-js-0.4.10.tgz", + "integrity": "sha512-JZoqlRSHtx+bc+xKI9o4bropEbqZBF4ZfYImiB1T9RYpHB73h5I8XZ7FfSBbHbBMtdD1c04ujjAPH8wUuu4+Gw==" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -11084,6 +11237,11 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "point-in-polygon": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.0.1.tgz", + "integrity": "sha1-1Ztk6P7kHElFiqyCtWcYxZV7Kvc=" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -11482,6 +11640,24 @@ "resolved": "https://registry.npmjs.org/react-native-branch/-/react-native-branch-3.0.1.tgz", "integrity": "sha512-vbcYxPZlpF5f39GAEUF8kuGQqCNeD3E6zEdvtOq8oCGZunHXlWlKgAS6dgBKCvsHvXgHuMtpvs39VgOp8DaKig==" }, + "react-native-chart-kit": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-native-chart-kit/-/react-native-chart-kit-4.3.0.tgz", + "integrity": "sha512-mVi0jT2bbKJOpmyqdn2+kGiInU+7P+ebi2ddQWm/Vfwbmd0InyPi7wj3F5UN9Ymr2tYQxuqpOJtHKmn1bT3/5w==", + "requires": { + "lodash": "^4.17.13", + "paths-js": "^0.4.10", + "point-in-polygon": "^1.0.1" + } + }, + "react-native-dotenv": { + "version": "0.2.0", + "resolved": "https://registry.npm.taobao.org/react-native-dotenv/download/react-native-dotenv-0.2.0.tgz", + "integrity": "sha1-MRVRy2o1o9z+3mSL3tVcDj7OV50=", + "requires": { + "babel-plugin-dotenv": "0.1.1" + } + }, "react-native-drawer": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/react-native-drawer/-/react-native-drawer-2.5.1.tgz", @@ -11573,6 +11749,15 @@ "react-native-modal": "^11.0.2" } }, + "react-native-modal-dropdown": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/react-native-modal-dropdown/-/react-native-modal-dropdown-0.7.0.tgz", + "integrity": "sha512-h2UrozBByQhL56XDboj/wjc/5Ny787eLQ++4ql7TecBdbLqbf+tlE62VeXKz30XVMN3iUVYUR/XmM/RIwLIXEg==", + "requires": { + "deprecated-react-native-listview": "0.0.5", + "prop-types": "^15.6.0" + } + }, "react-native-ratings": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/react-native-ratings/-/react-native-ratings-6.5.0.tgz", @@ -11587,6 +11772,11 @@ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-1.2.0.tgz", "integrity": "sha512-vkWRHrPK5qfHP/ZawlRoo38oeYe9NZaaOH/lmFxRcsKzaSK6x3H5ZPXI8lK6MfTLveqwo1QhJje3zIKXO4nQQw==" }, + "react-native-safe-area-context": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-0.6.2.tgz", + "integrity": "sha512-VtBW0JymVCwVt0TGxTv25Co3wtOv3Yb/K5pAUYkjKXNhWFDz5ZwUG/Yho/p0EkWYlFdroJcSqlvVFl2n3AU7SA==" + }, "react-native-safe-area-view": { "version": "0.14.8", "resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.14.8.tgz", @@ -11603,9 +11793,9 @@ } }, "react-native-screens": { - "version": "1.0.0-alpha.23", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-1.0.0-alpha.23.tgz", - "integrity": "sha512-tOxHGQUN83MTmQB4ghoQkibqOdGiX4JQEmeyEv96MKWO/x8T2PJv84ECUos9hD3blPRQwVwSpAid1PPPhrVEaw==", + "version": "2.0.0-alpha.22", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-2.0.0-alpha.22.tgz", + "integrity": "sha512-2U++QrTf8H989ekHbgFuia8LLd8/+SbXra+rqDAOihCNRLFi91+y5QGgc7DP4Ic9MtHTaYRtWopyfyUo4ybD0A==", "requires": { "debounce": "^1.2.0" } @@ -11615,6 +11805,15 @@ "resolved": "https://registry.npmjs.org/react-native-status-bar-height/-/react-native-status-bar-height-2.4.0.tgz", "integrity": "sha512-pWvZFlyIHiuxLugLioq97vXiaGSovFXEyxt76wQtbq0gxv4dGXMPqYow46UmpwOgeJpBhqL1E0EKxnfJRrFz5w==" }, + "react-native-svg": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-10.0.0.tgz", + "integrity": "sha512-7x56ji3oP8+Bh1jVuBtrq/JGxqLBAVf43BCuAbPMKaBlq78TXCMuln7fiBxCUOD4Z5hoZZaVkILhrJPPszfENA==", + "requires": { + "css-select": "^2.0.2", + "css-tree": "^1.0.0-alpha.37" + } + }, "react-native-tab-view": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-2.11.0.tgz", @@ -11808,6 +12007,22 @@ } } }, + "react-native-webview": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-8.0.3.tgz", + "integrity": "sha512-Uhd3o73HcqiQ0GWxyz5G5+Jkh1dd1A8Kz/rCYHiX/Ctkg3KO9qrZUmzRetq5y9X1+fUH9XJt6+LO3iO/Dxopfw==", + "requires": { + "escape-string-regexp": "2.0.0", + "invariant": "2.2.4" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, "react-navigation": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/react-navigation/-/react-navigation-4.0.10.tgz", @@ -11817,6 +12032,11 @@ "@react-navigation/native": "^3.6.2" } }, + "react-navigation-hooks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-navigation-hooks/-/react-navigation-hooks-1.1.0.tgz", + "integrity": "sha512-ZY/aiYJ88KXaOo8iOa4171O/0x6ztGhUPd2OYzdaJhLT/tP64zi5HB/RZFImuKhaBTODXjoSpFaOTA5xpePG4g==" + }, "react-navigation-stack": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/react-navigation-stack/-/react-navigation-stack-1.10.3.tgz", diff --git a/package.json b/package.json index b12690a..134dca9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@expo/vector-icons": "^10.0.6", + "dotenv": "^8.2.0", "expo": "^35.0.0", "expo-font": "~7.0.0", "expo-linear-gradient": "~7.0.0", @@ -26,12 +27,20 @@ "react": "16.9.0", "react-dom": "16.9.0", "react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz", + "react-native-chart-kit": "^4.3.0", + "react-native-dotenv": "^0.2.0", "react-native-elements": "^1.2.7", "react-native-gesture-handler": "~1.3.0", "react-native-modal-datetime-picker": "^7.6.1", + "react-native-modal-dropdown": "^0.7.0", "react-native-reanimated": "~1.2.0", + "react-native-safe-area-context": "^0.6.1", + "react-native-screens": "^2.0.0-alpha.22", + "react-native-svg": "^10.0.0", "react-native-web": "^0.11.7", + "react-native-webview": "^8.0.3", "react-navigation": "^4.0.10", + "react-navigation-hooks": "^1.1.0", "react-navigation-stack": "^1.10.3", "react-navigation-tabs": "^2.6.2", "react-redux": "^7.1.3", diff --git a/src/App.jsx b/src/App.jsx index 4429864..5d3b063 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,16 +2,13 @@ import React, { useState, useEffect } from 'react'; import { registerRootComponent, AppLoading } from 'expo'; import * as Font from 'expo-font'; import { Provider } from 'react-redux'; -import { createAppContainer } from 'react-navigation'; import { Alert } from 'react-native'; import { PersistGate } from 'redux-persist/integration/react'; -import AppNavigator from './Navigator'; +import AppContainer from './AppContainer'; import { store, persistor } from './reduxStore'; import Roboto from '../assets/Fonts/Roboto/Roboto-Regular.ttf'; import RobotoMedium from '../assets/Fonts/Roboto/Roboto-Medium.ttf'; -const AppContainer = createAppContainer(AppNavigator); - const App = () => { const [fontLoading, setFontLoading] = useState(true); diff --git a/src/AppContainer.jsx b/src/AppContainer.jsx new file mode 100644 index 0000000..44c84e5 --- /dev/null +++ b/src/AppContainer.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { createAppContainer } from 'react-navigation'; +import AppNavigatorWhite from './Navigator/indexWhite'; +import AppNavigatorBlack from './Navigator/indexBlack'; +import AppNavigatorWhiteProfile from './Navigator/indexWhiteProfile'; +import AppNavigatorBlackProfile from './Navigator/indexBlackProfile'; + +let assigned = false; +export default () => { + const { themeMode } = useSelector((state) => state.theme); + let AppContainer; + if (assigned) { + AppContainer = themeMode === 'Light' ? createAppContainer(AppNavigatorWhiteProfile) : createAppContainer(AppNavigatorBlackProfile); + } else { + AppContainer = themeMode === 'Light' ? createAppContainer(AppNavigatorWhite) : createAppContainer(AppNavigatorBlack); + } + assigned = true; + return <AppContainer />; +}; diff --git a/src/Chart/LineGraph.jsx b/src/Chart/LineGraph.jsx new file mode 100644 index 0000000..6ae6e56 --- /dev/null +++ b/src/Chart/LineGraph.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { View, Dimensions } from 'react-native'; +import { LineChart } from 'react-native-chart-kit'; +import PropTypes from 'prop-types'; + +export default function LineGraph({ dataSet }) { + const screenWidth = Dimensions.get('window').width * 0.85; + + const chartConfig = { + backgroundGradientFrom: '#e96d9e', + backgroundGradientTo: '#ffaa8f', + color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, + strokeWidth: 2, + }; + + const data = { + labels: dataSet.labels, + datasets: [{ + data: dataSet.total, + color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, + strokeWidth: 2, + }], + }; + return ( + <View> + <LineChart + style={{ flex: 1 }} + fromZero + data={data} + height={220} + width={screenWidth} + chartConfig={chartConfig} + bezier + /> + </View> + ); +} + +LineGraph.propTypes = { + dataSet: PropTypes.shape({ labels: PropTypes.array, total: PropTypes.array }), +}; + +LineGraph.defaultProps = { + dataSet: {}, +}; diff --git a/src/Chart/PieGraph.jsx b/src/Chart/PieGraph.jsx new file mode 100644 index 0000000..2ae63cc --- /dev/null +++ b/src/Chart/PieGraph.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Animated, View, Easing } from 'react-native'; +import { PieChart } from 'react-native-chart-kit'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import PieLegend from './PieLegend'; +import setThemeStyle from '../Common/theme/setThemeStyle'; + +export default function PieGraph({ graphDataSet }) { + const { themeMode } = useSelector((state) => state.theme); + const styles = setThemeStyle(themeMode); + const spinValue = new Animated.Value(0); + const { dataSet, legendSet } = graphDataSet; + Animated.timing(spinValue, { + toValue: 1, + duration: 900, + easing: Easing.bounce, + }).start(); + + const spin = spinValue.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '90deg'], + }); + + dataSet.forEach((item) => { + const value = item; + value.legendFontColor = styles.mainColor.color; + return value; + }); + + const chartConfig = { + backgroundGradientFrom: '#e96d9e', + backgroundGradientTo: '#ffaa8f', + decimalPlaces: 2, + color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`, + strokeWidth: 2, + }; + + return ( + <View style={{ flex: 1 }}> + <Animated.View style={{ transform: [{ rotate: spin }], justifyContent: 'center', alignItems: 'center' }}> + <PieChart + data={dataSet} + width={220} + height={220} + accessor="total" + backgroundColor="transparent" + chartConfig={chartConfig} + paddingLeft="55" + hasLegend={false} + /> + </Animated.View> + <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', flex: 1, flexWrap: 'wrap' }}> + {legendSet.map((item, idx) => <PieLegend key={`PieLegend:${idx + 1}`} text={item.name} color={item.color} />)} + </View> + </View> + ); +} + +PieGraph.propTypes = { + graphDataSet: PropTypes.shape({ + dataSet: PropTypes.array, + legendSet: PropTypes.array, + }).isRequired, +}; diff --git a/src/Chart/PieLegend.jsx b/src/Chart/PieLegend.jsx new file mode 100644 index 0000000..2f04c95 --- /dev/null +++ b/src/Chart/PieLegend.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { View, Text } from 'react-native'; +import { Icon } from 'react-native-elements'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import setThemeStyle from '../Common/theme/setThemeStyle'; + +export default function PieLegend({ text, color }) { + const { themeMode } = useSelector((state) => state.theme); + const styles = setThemeStyle(themeMode); + return ( + <View style={{ flexDirection: 'row', alignItems: 'center' }}> + <Icon type="octicon" color={color} name="primitive-dot" /> + <Text style={{ paddingLeft: 4, paddingRight: 6, color: styles.mainColor.color }}>{text}</Text> + </View> + ); +} + +PieLegend.propTypes = { + text: PropTypes.string, + color: PropTypes.string, +}; + +PieLegend.defaultProps = { + text: '', + color: '', +}; diff --git a/src/Chart/chartUtils.js b/src/Chart/chartUtils.js new file mode 100644 index 0000000..dbcba35 --- /dev/null +++ b/src/Chart/chartUtils.js @@ -0,0 +1,72 @@ +const moment = require('moment'); + +const convertToLineGraphDataset = (dataList, dataType) => { + const labels = []; + const total = []; + dataList.map((groupedRecordsByDate) => { + const filterData = groupedRecordsByDate.filter((item) => item.type === dataType); + const result = filterData.reduce((sum, { amount }) => { + const parsedAmount = parseFloat(amount); + return sum + parsedAmount; + }, 0); + if (filterData.length !== 0) { + const formatDate = moment(groupedRecordsByDate[0].date, 'MMMM Do YYYY').format('D'); + labels.push(formatDate); + total.push(result); + } + return null; + }); + return { labels: labels.reverse(), total: total.reverse() }; +}; + +const convertToDatasetByCategory = (dataList, dataType) => { + const filterData = dataList.filter((item) => item.type === dataType); + if (filterData.length !== 0) { + const dataSet = []; + const colorSet = []; + const legendSet = []; + filterData.forEach((item) => { + const index = dataSet.findIndex((value) => value.name === item.labelName.name); + if (index === -1) { + dataSet.push({ + name: item.labelName.name, + total: parseFloat(item.amount), + color: item.labelName.color, + }); + colorSet.push(item.labelName.color); + legendSet.push({ + name: item.labelName.name, + color: item.labelName.color, + }); + } else { + const newValue = parseFloat(dataSet[index].total) + parseFloat(item.amount); + dataSet[index].total = newValue; + } + }); + return { dataSet, legendSet }; + } + return []; +}; + +const filterDataByPeriod = (dataList, range, view) => { + const monthRange = range.format('MMM YYYY'); + const yearRange = range.format('YYYY'); + let result; + switch (view) { + case 'month': + result = dataList.filter((value) => moment.unix(value.date).format('MMM YYYY') === monthRange); + break; + case 'year': + result = dataList.filter((value) => moment.unix(value.date).format('YYYY') === yearRange); + break; + default: + break; + } + return result; +}; + +export default { + convertToLineGraphDataset, + convertToDatasetByCategory, + filterDataByPeriod, +}; diff --git a/src/Chart/index.jsx b/src/Chart/index.jsx new file mode 100644 index 0000000..0dfce16 --- /dev/null +++ b/src/Chart/index.jsx @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; +import { View, ScrollView } from 'react-native'; +import { useSelector } from 'react-redux'; +import MainHeader from '../Common/MainHeader'; +import DateSlider from '../Common/DateSlider'; +import DateOverlay from '../Common/DateOverlay'; +import setThemeStyle from '../Common/theme/setThemeStyle'; +import EmptyHistory from '../Stats/EmptyHistory'; +import utils from '../Stats/utils'; +import statsUtils from './chartUtils'; +import LineGraph from './LineGraph'; +import PieGraph from './PieGraph'; + +const moment = require('moment'); + +export default function Chart() { + const { transactions } = useSelector((state) => state.transactions); + const { themeMode } = useSelector((state) => state.theme); + const styles = setThemeStyle(themeMode); + const [view, setCurrentView] = useState('month'); + const [timePeriodOptions, setTimePeriod] = useState(utils.getDateSet(moment(), view)); + const [isOverlayVisible, setOverlayVisibility] = useState(false); + const updateHeaderView = (type) => { + setCurrentView(type); + setTimePeriod(utils.getDateSet(moment(), type)); + setOverlayVisibility(!isOverlayVisible); + }; + + const filterListView = (transactionRecords) => { + const current = timePeriodOptions[1]; + return utils.filterData(transactionRecords, current, view); + }; + + + const navBarFunc = [ + { + name: 'filter', + func: () => setOverlayVisibility(true), + }, + ]; + + const filteredDataByDate = filterListView(utils.groupTransactionsByDate(transactions)); + + const filterDataByPeriod = statsUtils.filterDataByPeriod( + transactions, timePeriodOptions[1], view, + ); + + const dataSetByDate = statsUtils.convertToLineGraphDataset(filteredDataByDate, 'Expense'); + const graphDataSet = statsUtils.convertToDatasetByCategory(filterDataByPeriod, 'Expense'); + + const isLineChartExist = filterDataByPeriod.length !== 0 && dataSetByDate.labels.length !== 0; + const isPieChartExist = filterDataByPeriod.length !== 0 && graphDataSet.length !== 0; + + return ( + <View style={{ flex: 1, flexDirection: 'column' }}> + <DateOverlay + isOverlayVisible={isOverlayVisible} + onPressBtn={(viewValue) => updateHeaderView(viewValue)} + onPressClose={() => setOverlayVisibility(false)} + /> + <MainHeader title="Charts" btnType={navBarFunc} /> + <DateSlider + viewSet={timePeriodOptions} + onPressBtn={(value, type) => setTimePeriod(utils.getDateSet(value, type))} + viewType={view} + /> + <ScrollView style={styles.deviceBody}> + {isLineChartExist && isPieChartExist ? ( + <> + <View style={styles.card}> + <LineGraph dataSet={dataSetByDate} style={{ flex: 1 }} /> + </View> + <View style={styles.card}> + <PieGraph graphDataSet={graphDataSet} style={{ flex: 1 }} /> + </View> + </> + ) + : <EmptyHistory />} + </ScrollView> + </View> + ); +} diff --git a/src/Common/Color.js b/src/Common/Color.js index 738352b..8483fc6 100644 --- a/src/Common/Color.js +++ b/src/Common/Color.js @@ -2,6 +2,9 @@ export default { navColor: { activeTintColor: '#5C6BC0', inactiveTintColor: 'gray', + style: { + backgroundColor: '#e0e0e0', + }, }, headerColor: { @@ -14,5 +17,7 @@ export default { green: ['#2fc899', '#78ef93'], purple: ['#a765cf', '#f8b6cc'], orange: ['#ef6d6b', '#feaa74'], + gray: ['#ababab', '#d1d1d1'], + white: ['#ffffff', '#ffffff'], }, }; diff --git a/src/Common/DateOverlay.jsx b/src/Common/DateOverlay.jsx new file mode 100644 index 0000000..3e93c92 --- /dev/null +++ b/src/Common/DateOverlay.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { View } from 'react-native'; +import { ListItem, Overlay, Button } from 'react-native-elements'; +import PropTypes from 'prop-types'; + +export default function DateOverlay(props) { + const { isOverlayVisible, onPressClose, onPressBtn } = props; + + return ( + <Overlay height={200} isVisible={isOverlayVisible}> + <View> + <View style={{ flexDirection: 'row', justifyContent: 'flex-end' }}> + <Button icon={{ name: 'close' }} type="clear" onPress={() => onPressClose(false)} /> + </View> + <View style={{ marginTop: 20 }}> + <ListItem title="Month" topDivider bottomDivider onPress={() => onPressBtn('month')} /> + <ListItem title="Year" bottomDivider onPress={() => onPressBtn('year')} /> + </View> + </View> + </Overlay> + ); +} + +DateOverlay.propTypes = { + isOverlayVisible: PropTypes.bool, + onPressClose: PropTypes.func.isRequired, + onPressBtn: PropTypes.func.isRequired, +}; + +DateOverlay.defaultProps = { + isOverlayVisible: false, +}; diff --git a/src/Stats/DateSlider.jsx b/src/Common/DateSlider.jsx similarity index 77% rename from src/Stats/DateSlider.jsx rename to src/Common/DateSlider.jsx index b038018..e50bf20 100644 --- a/src/Stats/DateSlider.jsx +++ b/src/Common/DateSlider.jsx @@ -1,15 +1,19 @@ import React from 'react'; import { View, Text } from 'react-native'; -import PropTypes from 'prop-types'; import { Button } from 'react-native-elements'; -import styles from '../Common/themeStyle'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import setThemeStyle from './theme/setThemeStyle'; + export default function DateSlider(props) { + const { themeMode } = useSelector((state) => state.theme); + const styles = setThemeStyle(themeMode); const { viewSet, onPressBtn, viewType } = props; const format = viewType === 'month' ? 'MMM YYYY' : 'YYYY'; return ( - <View style={{ flexDirection: 'row', justifyContent: 'space-around' }}> + <View style={styles.dateContainer}> <Button containerStyle={styles.dateNormal} title={viewSet[0].format(format)} @@ -18,7 +22,7 @@ export default function DateSlider(props) { onPress={() => onPressBtn(viewSet[0], viewType)} /> <View style={styles.dateHighlight}> - <Text style={{ textAlign: 'center', fontSize: 18, fontWeight: '500' }}>{viewSet[1].format(format)}</Text> + <Text style={styles.dateText}>{viewSet[1].format(format)}</Text> </View> <Button containerStyle={styles.dateNormal} diff --git a/src/Common/LargeBtn.jsx b/src/Common/LargeBtn.jsx index da02706..9d697d2 100644 --- a/src/Common/LargeBtn.jsx +++ b/src/Common/LargeBtn.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { View, Text, TouchableOpacity } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import PropTypes from 'prop-types'; -import styles from './themeStyle'; +import styles from './themeStyleLight'; import themeColor from './Color'; export default function LargeButton({ @@ -20,9 +20,11 @@ export default function LargeButton({ <TouchableOpacity onPress={pressHandler}> <LinearGradient colors={themeColor.gradientColor[bgColor]} - start={[0.1, 0.9]} + start={[0.3, 0.7]} end={[0.9, 0.1]} - style={{ borderRadius: 10, padding: 20 }} + style={{ + borderRadius: 10, paddingLeft: 20, paddingRight: 20, paddingTop: 13, paddingBottom: 13, + }} > <Text style={styles.largeBtnFont}>{subtitle}</Text> <Text style={styles.largeBtnHeader}> diff --git a/src/Common/MainHeader.jsx b/src/Common/MainHeader.jsx index 47b47a2..36adb1d 100644 --- a/src/Common/MainHeader.jsx +++ b/src/Common/MainHeader.jsx @@ -2,21 +2,26 @@ import React from 'react'; import { View, Text } from 'react-native'; import PropTypes from 'prop-types'; import { Button } from 'react-native-elements'; -import styles from './themeStyle'; +import { useSelector } from 'react-redux'; +import setThemeStyle from './theme/setThemeStyle'; export default function MainHeader(props) { - const { title, onPressBtn, btnName } = props; + const { title, btnType } = props; + const { themeMode } = useSelector((state) => state.theme); + const styles = setThemeStyle(themeMode); + return ( <View style={styles.mainHeader}> - <View style={{ flex: 1 }}> + <View style={{ flex: 1, alignItems: 'flex-start', marginBottom: -10 }}> <Button type="clear" /> </View> <Text style={[styles.headerFont, { flex: 1 }]}>{title}</Text> <View style={{ - flex: 1, alignItems: 'flex-end', + flex: 1, alignContent: 'flex-end', marginBottom: -10, justifyContent: 'flex-end', flexDirection: 'row', alignItems: 'flex-end', }} > - <Button onPress={onPressBtn} icon={{ name: btnName }} type="clear" /> + {(btnType.length) + ? btnType.map((item, index) => <Button key={`btnTypeGroup:${index + 1}`} onPress={item.func} icon={{ name: item.name, color: styles.mainColor.color }} type="clear" />) : <View />} </View> </View> ); @@ -24,12 +29,10 @@ export default function MainHeader(props) { MainHeader.propTypes = { title: PropTypes.string, - onPressBtn: PropTypes.func, - btnName: PropTypes.string, + btnType: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string, func: PropTypes.func })), }; MainHeader.defaultProps = { title: '', - onPressBtn: () => {}, - btnName: '', + btnType: [], }; diff --git a/src/Common/theme/actionCreator.js b/src/Common/theme/actionCreator.js new file mode 100644 index 0000000..68ab953 --- /dev/null +++ b/src/Common/theme/actionCreator.js @@ -0,0 +1,10 @@ +export const actionType = { + UPDATE_MODE: 'UPDATE_MODE', +}; + +export function updateMode(mode) { + return { + type: actionType.UPDATE_MODE, + mode, + }; +} diff --git a/src/Common/theme/setThemeStyle.js b/src/Common/theme/setThemeStyle.js new file mode 100644 index 0000000..d9e4f92 --- /dev/null +++ b/src/Common/theme/setThemeStyle.js @@ -0,0 +1,20 @@ +const setThemeStyle = (mode) => { + let style; + switch (mode) { + case 'Light': + style = require('../themeStyleLight'); + break; + case 'Dark': + style = require('../themeStyleDark'); + break; + case 'EPAM': + style = require('../themeStyleEpam'); + break; + default: + style = require('../themeStyleLight'); + break; + } + return style.default; +}; + +export default setThemeStyle; diff --git a/src/Common/theme/themeReducer.js b/src/Common/theme/themeReducer.js new file mode 100644 index 0000000..f3704f4 --- /dev/null +++ b/src/Common/theme/themeReducer.js @@ -0,0 +1,19 @@ +import { actionType } from './actionCreator'; + +const initialTheme = { + themeMode: 'Light', +}; + +const theme = (state = initialTheme, action) => { + switch (action.type) { + case actionType.UPDATE_MODE: + return { + ...state, + themeMode: action.mode, + }; + default: + return state; + } +}; + +export default theme; diff --git a/src/Common/themeStyleDark.js b/src/Common/themeStyleDark.js new file mode 100644 index 0000000..588b7f4 --- /dev/null +++ b/src/Common/themeStyleDark.js @@ -0,0 +1,252 @@ +import { StyleSheet } from 'react-native'; + +const commonHeader = { + flexDirection: 'row', + alignItems: 'flex-end', + paddingLeft: 20, + paddingRight: 20, + paddingBottom: 20, +}; + +const themeBtnDefault = { + height: 35, + width: 60, + borderRadius: 0, + paddingTop: 2, + paddingBottom: 2, +}; + +const defaultColor = { + cardColor: '#3b3b75', + secondaryHeading: '#8f8fb5', + listHeading: '#e0e0e0', + darkerColor: '#181a35', +}; + + +const styles = StyleSheet.create({ + mainColor: { + color: '#e0e0e0', + backgroundColor: '#3b3b75', + borderColor: 'gray', + }, + + deviceHead: { + height: 120, + justifyContent: 'flex-end', + backgroundColor: 'white', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.8, + shadowRadius: 2, + elevation: 5, + }, + + deviceBody: { + flexDirection: 'column', + paddingLeft: 20, + paddingRight: 20, + backgroundColor: defaultColor.darkerColor, + }, + + iconCard: { + backgroundColor: defaultColor.cardColor, + borderBottomLeftRadius: 6, + borderBottomRightRadius: 6, + shadowColor: 'grey', + shadowOffset: { + width: 2, + height: 3, + }, + shadowOpacity: 0.08, + shadowRadius: 5, + elevation: 2, + padding: 10, + marginTop: 20, + }, + + card: { + backgroundColor: defaultColor.cardColor, + borderRadius: 6, + shadowColor: 'grey', + shadowOffset: { + width: 2, + height: 3, + }, + shadowOpacity: 0.08, + shadowRadius: 5, + elevation: 2, + padding: 10, + marginTop: 20, + }, + + mainHeader: { + ...commonHeader, + height: 100, + justifyContent: 'space-between', + backgroundColor: defaultColor.cardColor, + }, + + mainHeaderModelight: { + ...commonHeader, + height: 100, + justifyContent: 'space-between', + }, + + headerFormat: { + flexDirection: 'row', + alignItems: 'flex-end', + paddingLeft: 20, + paddingRight: 20, + paddingBottom: 20, + backgroundColor: defaultColor.cardColor, + }, + + dateContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + backgroundColor: defaultColor.cardColor, + }, + + dateText: { + color: defaultColor.listHeading, + textAlign: 'center', + fontSize: 18, + fontWeight: '500', + }, + + dateNormal: { + flex: 1, + borderBottomWidth: 2, + borderBottomColor: defaultColor.cardColor, + }, + + dateHighlight: { + flex: 1, + borderBottomWidth: 2, + borderBottomColor: '#30d29d', + justifyContent: 'center', + }, + + firstHeading: { + fontSize: 40, + fontWeight: '400', + textAlign: 'center', + letterSpacing: 5, + textShadowColor: '#f8f8f8', + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 4, + }, + + secondaryHeading: { + fontSize: 20, + fontWeight: '500', + marginTop: 10, + marginBottom: 10, + color: defaultColor.secondaryHeading, + }, + + listHeading: { + color: defaultColor.listHeading, + }, + + cardAlign: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + + cardHeader: { + color: defaultColor.secondaryHeading, + fontSize: 18, + margin: 10, + }, + + redLargeBtn: { + marginTop: 20, + shadowColor: '#f38aa9', + }, + + greenLargeBtn: { + marginTop: 20, + shadowColor: '#70ea93', + }, + + largeBtnShadow: { + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.70, + shadowRadius: 4.65, + elevation: 8, + }, + + grayBtnShadow: { + shadowColor: '#cccccc', + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.70, + shadowRadius: 4.65, + elevation: 8, + }, + + overlayBtn: { + paddingTop: 5, + paddingBottom: 5, + paddingLeft: 15, + paddingRight: 15, + borderRadius: 25, + }, + + overlayText: { + fontSize: 20, + color: 'white', + textAlign: 'center', + }, + + largeBtnHeader: { + color: 'white', + fontSize: 18, + fontWeight: '400', + marginTop: 5, + marginBottom: 5, + }, + + largeBtnFont: { + color: 'white', + fontSize: 15, + fontWeight: '300', + }, + + labelHeader: { + height: 140, + justifyContent: 'space-between', + }, + + labelHeaderFont: { + fontSize: 20, + fontWeight: '400', + }, + + headerFont: { + fontSize: 20, + fontWeight: '500', + textAlign: 'center', + color: defaultColor.listHeading, + }, + + themeBtn: { + ...themeBtnDefault, + borderColor: '#5C6BC0', + }, + + disabledThemeBtn: { + ...themeBtnDefault, + backgroundColor: '#5C6BC0', + }, + +}); + +export default styles; diff --git a/src/Common/themeStyle.js b/src/Common/themeStyleEpam.js similarity index 57% rename from src/Common/themeStyle.js rename to src/Common/themeStyleEpam.js index 6dbfaac..fe7f248 100644 --- a/src/Common/themeStyle.js +++ b/src/Common/themeStyleEpam.js @@ -8,7 +8,26 @@ const commonHeader = { paddingBottom: 20, }; +const themeBtnDefault = { + height: 35, + width: 60, + borderRadius: 0, + paddingTop: 2, + paddingBottom: 2, +}; + +const defaultColor = { + secondaryHeading: 'blue', + listHeading: 'green', +}; + + const styles = StyleSheet.create({ + mainColor: { + color: 'black', + backgroundColor: 'white', + borderColor: '#f0f0f0', + }, deviceHead: { height: 120, justifyContent: 'flex-end', @@ -29,7 +48,7 @@ const styles = StyleSheet.create({ card: { backgroundColor: 'white', - borderRadius: 5, + borderRadius: 6, shadowColor: 'grey', shadowOffset: { width: 2, @@ -48,6 +67,13 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', }, + mainHeaderModelight: { + ...commonHeader, + height: 100, + justifyContent: 'space-between', + backgroundColor: '#262244', + }, + headerFormat: { flexDirection: 'row', alignItems: 'flex-end', @@ -56,6 +82,18 @@ const styles = StyleSheet.create({ paddingBottom: 20, }, + dateContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + }, + + dateText: { + color: defaultColor.listHeading, + textAlign: 'center', + fontSize: 18, + fontWeight: '500', + }, + dateNormal: { flex: 1, borderBottomWidth: 2, @@ -69,6 +107,28 @@ const styles = StyleSheet.create({ justifyContent: 'center', }, + firstHeading: { + fontSize: 40, + fontWeight: '400', + textAlign: 'center', + letterSpacing: 5, + textShadowColor: '#f8f8f8', + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 4, + }, + + secondaryHeading: { + fontSize: 20, + fontWeight: '500', + marginTop: 10, + marginBottom: 10, + color: defaultColor.secondaryHeading, + }, + + listHeading: { + color: defaultColor.listHeading, + }, + cardAlign: { flexDirection: 'row', justifyContent: 'space-between', @@ -100,6 +160,31 @@ const styles = StyleSheet.create({ elevation: 8, }, + grayBtnShadow: { + shadowColor: '#cccccc', + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.70, + shadowRadius: 4.65, + elevation: 8, + }, + + overlayBtn: { + paddingTop: 5, + paddingBottom: 5, + paddingLeft: 15, + paddingRight: 15, + borderRadius: 25, + }, + + overlayText: { + fontSize: 20, + color: 'white', + textAlign: 'center', + }, + largeBtnHeader: { color: 'white', fontSize: 18, @@ -130,6 +215,16 @@ const styles = StyleSheet.create({ textAlign: 'center', }, + themeBtn: { + ...themeBtnDefault, + borderColor: '#5C6BC0', + }, + + disabledThemeBtn: { + ...themeBtnDefault, + backgroundColor: '#5C6BC0', + }, + }); export default styles; diff --git a/src/Common/themeStyleLight.js b/src/Common/themeStyleLight.js new file mode 100644 index 0000000..92af6f2 --- /dev/null +++ b/src/Common/themeStyleLight.js @@ -0,0 +1,239 @@ +import { StyleSheet } from 'react-native'; + +const commonHeader = { + flexDirection: 'row', + alignItems: 'flex-end', + paddingLeft: 20, + paddingRight: 20, + paddingBottom: 20, +}; + +const themeBtnDefault = { + height: 35, + width: 60, + borderRadius: 0, + paddingTop: 2, + paddingBottom: 2, +}; + +const defaultColor = { + listHeading: 'black', +}; + +const styles = StyleSheet.create({ + mainColor: { + color: 'black', + backgroundColor: 'white', + borderColor: '#f0f0f0', + }, + + deviceHead: { + height: 120, + justifyContent: 'flex-end', + backgroundColor: 'white', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.8, + shadowRadius: 2, + elevation: 5, + }, + + deviceBody: { + flexDirection: 'column', + backgroundColor: '#f3f5fa', + paddingLeft: 20, + paddingRight: 20, + }, + + iconCard: { + backgroundColor: 'white', + borderBottomLeftRadius: 6, + borderBottomRightRadius: 6, + shadowColor: 'grey', + shadowOffset: { + width: 2, + height: 3, + }, + shadowOpacity: 0.08, + shadowRadius: 5, + elevation: 2, + padding: 10, + marginTop: 20, + }, + + card: { + backgroundColor: 'white', + borderRadius: 6, + shadowColor: 'grey', + shadowOffset: { + width: 2, + height: 3, + }, + shadowOpacity: 0.08, + shadowRadius: 5, + elevation: 2, + padding: 10, + marginTop: 20, + }, + + mainHeader: { + ...commonHeader, + height: 100, + justifyContent: 'space-between', + }, + + headerFormat: { + flexDirection: 'row', + alignItems: 'flex-end', + paddingLeft: 20, + paddingRight: 20, + paddingBottom: 20, + }, + + dateContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + }, + + dateText: { + color: defaultColor.listHeading, + textAlign: 'center', + fontSize: 18, + fontWeight: '500', + }, + + dateNormal: { + flex: 1, + borderBottomWidth: 2, + borderBottomColor: 'white', + }, + + dateHighlight: { + flex: 1, + borderBottomWidth: 2, + borderBottomColor: '#30d29d', + justifyContent: 'center', + }, + + firstHeading: { + fontSize: 40, + fontWeight: '400', + textAlign: 'center', + letterSpacing: 5, + textShadowColor: '#f8f8f8', + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 4, + }, + + secondaryHeading: { + fontSize: 20, + fontWeight: '500', + marginTop: 10, + marginBottom: 10, + color: 'grey', + }, + + + listHeading: { + color: defaultColor.listHeading, + }, + + cardAlign: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + + cardHeader: { + color: '#777777', + fontSize: 18, + margin: 10, + }, + + redLargeBtn: { + marginTop: 20, + shadowColor: '#f38aa9', + }, + + greenLargeBtn: { + marginTop: 20, + shadowColor: '#70ea93', + }, + + largeBtnShadow: { + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.70, + shadowRadius: 4.65, + elevation: 8, + }, + + grayBtnShadow: { + shadowColor: '#cccccc', + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.70, + shadowRadius: 4.65, + elevation: 8, + }, + + overlayBtn: { + paddingTop: 5, + paddingBottom: 5, + paddingLeft: 15, + paddingRight: 15, + borderRadius: 25, + }, + + overlayText: { + fontSize: 20, + color: 'white', + textAlign: 'center', + }, + + largeBtnHeader: { + color: 'white', + fontSize: 18, + fontWeight: '400', + marginTop: 5, + marginBottom: 5, + }, + + largeBtnFont: { + color: 'white', + fontSize: 15, + fontWeight: '300', + }, + + labelHeader: { + height: 140, + justifyContent: 'space-between', + }, + + labelHeaderFont: { + fontSize: 20, + fontWeight: '400', + }, + + headerFont: { + fontSize: 20, + fontWeight: '500', + textAlign: 'center', + }, + + themeBtn: { + ...themeBtnDefault, + borderColor: '#5C6BC0', + }, + + disabledThemeBtn: { + ...themeBtnDefault, + backgroundColor: '#5C6BC0', + }, + +}); + +export default styles; diff --git a/src/FamilyTrans/actionCreator.js b/src/FamilyTrans/actionCreator.js new file mode 100644 index 0000000..57b487b --- /dev/null +++ b/src/FamilyTrans/actionCreator.js @@ -0,0 +1,94 @@ +import { BACKEND_URL } from 'react-native-dotenv'; +// const BACKEND_URL = 'http://10.72.160.173:8080'; + +export const actionType = { + ADD_FAMILY_TRANS_FAILED: 'ADD_FAMILY_TRANS_FAILED', + GET_FAMILY_TRANS_START: 'GET_FAMILY_TRANS_START', + GET_FAMILY_TRANS: 'GET_FAMILY_TRANS', + GET_FAMILY_TRANS_FAILED: 'GET_FAMILY_TRANS_FAILED', + GET_FAMILY_TRANS_END: 'GET_FAMILY_TRANS_END', +}; + +export function addFamilyTransFailed(error) { + return { + type: actionType.ADD_FAMILY_TRANS_FAILED, + error, + }; +} + +export function getFamilyTransStart() { + return { + type: actionType.GET_FAMILY_TRANS_START, + }; +} + +export function getFamilyTrans(dataSet) { + return { + type: actionType.GET_FAMILY_TRANS, + dataSet, + }; +} + +export function getFamilyTransFailed(error) { + return { + type: actionType.GET_FAMILY_TRANS_FAILED, + error, + }; +} + +export const addFamilyTransactions = ({ transaction }) => (dispatch, getState) => { + const { accessToken } = getState().user; + if (!accessToken) { + dispatch(addFamilyTransFailed('Unauthorized token')); + return; + } + fetch(`${BACKEND_URL}/family-transactions`, { + method: 'POST', + mode: 'cors', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(transaction), + }).then((response) => { + if (response !== 200) { + return response.json(); + } + }).then((response) => { + if (response === undefined) { + dispatch(addFamilyTransFailed('Unauthroized error')); + } + dispatch(addFamilyTransFailed(response.message)); + }).catch(() => { + dispatch(addFamilyTransFailed('Network error, please try again later')); + }); +}; + +export const getFamilyTransactions = () => (dispatch, getState) => { + const { accessToken } = getState().user; + dispatch(getFamilyTransStart()); + if (!accessToken) { + dispatch(getFamilyTransFailed('Unauthorized token')); + return; + } + fetch(`${BACKEND_URL}/family-transactions`, { + method: 'GET', + mode: 'cors', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }).then((response) => { + return response.json(); + }).then((response) => { + if (response === undefined) { + dispatch(getFamilyTransFailed('Unauthorized error')); + } else if (response.code === 200) { + dispatch(getFamilyTrans(response.data)); + } else { + dispatch(getFamilyTransFailed(response.message)); + } + }).catch(() => { + dispatch(getFamilyTransFailed('Network error, please try again later')); + }); +}; diff --git a/src/FamilyTrans/familyTransReducer.js b/src/FamilyTrans/familyTransReducer.js new file mode 100644 index 0000000..39ea776 --- /dev/null +++ b/src/FamilyTrans/familyTransReducer.js @@ -0,0 +1,32 @@ +import { actionType } from './actionCreator'; + +const initialState = { + transactions: [], + error: '', + isInProgress: false, +}; + +export default (state = initialState, action) => { + switch (action.type) { + case actionType.GET_FAMILY_TRANS_START: + return { + ...state, + isInProgress: true, + }; + case actionType.GET_FAMILY_TRANS: + return { + ...state, + transactions: action.dataSet, + isInProgress: false, + error: '', + }; + case actionType.GET_FAMILY_TRANS_FAILED: + return { + ...state, + error: action.error, + isInProgress: false, + }; + default: + return state; + } +}; diff --git a/src/FamilyTrans/index.jsx b/src/FamilyTrans/index.jsx new file mode 100644 index 0000000..be1cec1 --- /dev/null +++ b/src/FamilyTrans/index.jsx @@ -0,0 +1,137 @@ +import React, { useState, useEffect } from 'react'; +import { + View, ScrollView, RefreshControl, Dimensions, ImageBackground, +} from 'react-native'; +import { useSelector, useDispatch } from 'react-redux'; +import { useNavigation } from 'react-navigation-hooks'; +import { getFamilyTransactions } from './actionCreator'; +import MainHeader from '../Common/MainHeader'; +import DateSlider from '../Common/DateSlider'; +import utils from '../Stats/utils'; +import FilterBtn from '../Stats/FilterBtn'; +import TransList from '../Stats/TransList'; +import EmptyHistory from '../Stats/EmptyHistory'; +import DateOverlay from '../Common/DateOverlay'; +import setThemeStyle from '../Common/theme/setThemeStyle'; +import SubmitBtn from '../Personal/SubmitBtn'; + +const moment = require('moment'); + +const bgImg = require('../../assets/blur.jpg'); + +export default function Trans() { + const { transactions, isInProgress } = useSelector((state) => state.familyTrans); + const { accessToken } = useSelector((state) => state.user); + const { themeMode } = useSelector((state) => state.theme); + const { navigate } = useNavigation(); + const dispatch = useDispatch(); + const styles = setThemeStyle(themeMode); + const [view, setCurrentView] = useState('month'); + const [timePeriodOptions, setTimePeriod] = useState(utils.getDateSet(moment(), view)); + const [isOverlayVisible, setOverlayVisibility] = useState(false); + const [transFilter, setTransFilter] = useState('all'); + + const screenWidth = Dimensions.get('window').width; + const screenHeight = Dimensions.get('window').height; + + + const onRefresh = React.useCallback(() => { + dispatch(getFamilyTransactions()); + }, [isInProgress]); + + + useEffect(() => { + dispatch(getFamilyTransactions()); + }, []); + + const updateHeaderView = (type) => { + setCurrentView(type); + setTimePeriod(utils.getDateSet(moment(), type)); + setOverlayVisibility(!isOverlayVisible); + }; + + const filterTransactions = (transactionsList) => { + const transactionsWithSpecificType = utils.filterTransactionByType( + transactionsList, + transFilter, + ); + return utils.filterTransactionsByDate( + transactionsWithSpecificType, + timePeriodOptions[1], + view, + ); + }; + + const calculateSumByType = (dataList, type) => dataList + .filter((item) => item.type === type) + .reduce((sum, { amount }) => sum + parseFloat(amount), 0); + + const filteredTransactions = filterTransactions(transactions); + const processedTransactions = utils.groupTransactionsByDate(filteredTransactions); + const totalExpense = calculateSumByType(filteredTransactions, 'Expense'); + const totalIncome = calculateSumByType(filteredTransactions, 'Income'); + const navBarFunc = [ + { + name: 'filter', + func: () => setOverlayVisibility(true), + }, + ]; + + return ( + <> + {(accessToken) ? ( + <View style={{ flex: 1, flexDirection: 'column' }}> + <DateOverlay + isOverlayVisible={isOverlayVisible} + onPressBtn={(viewValue) => updateHeaderView(viewValue)} + onPressClose={() => setOverlayVisibility(false)} + /> + <MainHeader title="Transactions" btnType={navBarFunc} /> + <DateSlider + viewSet={timePeriodOptions} + onPressBtn={(value, type) => setTimePeriod(utils.getDateSet(value, type))} + viewType={view} + /> + <ScrollView + style={styles.deviceBody} + refreshControl={ + <RefreshControl refreshing={isInProgress} onRefresh={onRefresh} /> + } + > + <View style={{ marginBottom: 20 }}> + <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}> + <FilterBtn + currentFilter={transFilter} + onFilterChange={(newFilter) => (newFilter === transFilter ? setTransFilter('all') : setTransFilter(newFilter))} + totalExpense={totalExpense} + totalIncome={totalIncome} + /> + </View> + {(transactions.length !== 0) + ? ( + <TransList + transactions={processedTransactions} + /> + ) + : <EmptyHistory />} + </View> + </ScrollView> + </View> + ) + : ( + <ImageBackground + source={bgImg} + style={{ + width: screenWidth, height: screenHeight, alignItems: 'center', justifyContent: 'center', + }} + > + <SubmitBtn + disabled={false} + onPressBtn={() => navigate('Me')} + title="Please Login To Unlock" + /> + </ImageBackground> + )} + </> + ); +} diff --git a/src/Navigator/TabBarIcon.jsx b/src/Navigator/TabBarIcon.jsx index 9fef735..e28f2ac 100644 --- a/src/Navigator/TabBarIcon.jsx +++ b/src/Navigator/TabBarIcon.jsx @@ -7,7 +7,11 @@ const TabBarIcon = ({ navigation, focused, tintColor }) => { const { routeName } = navigation.state; let iconName; let IconComponent; switch (routeName) { - case 'Stats': + case 'Trans': + IconComponent = MaterialCommunityIcons; + iconName = 'format-list-bulleted'; + break; + case 'Chart': IconComponent = MaterialIcons; iconName = `pie-chart${focused ? '' : '-outlined'}`; break; @@ -15,6 +19,10 @@ const TabBarIcon = ({ navigation, focused, tintColor }) => { IconComponent = MaterialCommunityIcons; iconName = `pencil-circle${focused ? '' : '-outline'}`; break; + case 'Family': + IconComponent = MaterialIcons; + iconName = `people${focused ? '' : '-outline'}`; + break; case 'Me': IconComponent = MaterialIcons; iconName = `person${focused ? '' : '-outline'}`; diff --git a/src/Navigator/TabBarIcon.test.jsx b/src/Navigator/TabBarIcon.test.jsx index 287c948..7c9ab3f 100644 --- a/src/Navigator/TabBarIcon.test.jsx +++ b/src/Navigator/TabBarIcon.test.jsx @@ -1,16 +1,35 @@ import React from 'react'; import * as Font from 'expo-font'; import renderer from 'react-test-renderer'; -import { MaterialIcons } from '@expo/vector-icons'; +import { MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons'; import TabBarIcon from './TabBarIcon'; -beforeAll(() => Font.loadAsync(MaterialIcons.font)); - describe('<TabBarIcon />', () => { - it('render', () => { + beforeEach(() => Font.loadAsync(MaterialIcons.font)); + it('render Trans', () => { + const tree = renderer.create(<TabBarIcon + navigation={{ state: { routeName: 'Trans' } }} + focused + tintColor="#5C6BC0" + />).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + beforeEach(() => Font.loadAsync(MaterialCommunityIcons.font)); + it('render Create', () => { + const tree = renderer.create(<TabBarIcon + navigation={{ state: { routeName: 'Create' } }} + focused + tintColor="#5C6BC0" + />).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + beforeEach(() => Font.loadAsync(MaterialIcons.font)); + it('render Me', () => { const tree = renderer.create(<TabBarIcon - navigation={{ state: { routeName: 'Stats' } }} + navigation={{ state: { routeName: 'Me' } }} focused tintColor="#5C6BC0" />).toJSON(); diff --git a/src/Navigator/__snapshots__/TabBarIcon.test.jsx.snap b/src/Navigator/__snapshots__/TabBarIcon.test.jsx.snap index f7b4d36..2159749 100644 --- a/src/Navigator/__snapshots__/TabBarIcon.test.jsx.snap +++ b/src/Navigator/__snapshots__/TabBarIcon.test.jsx.snap @@ -1,6 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`<TabBarIcon /> render 1`] = ` +exports[`<TabBarIcon /> render Create 1`] = ` +<Text + allowFontScaling={false} + style={ + Array [ + Object { + "color": "#5C6BC0", + "fontSize": 25, + }, + undefined, + Object { + "fontFamily": "material-community", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } +> +  +</Text> +`; + +exports[`<TabBarIcon /> render Me 1`] = ` <Text allowFontScaling={false} style={ @@ -19,6 +42,29 @@ exports[`<TabBarIcon /> render 1`] = ` ] } > -  +  +</Text> +`; + +exports[`<TabBarIcon /> render Trans 1`] = ` +<Text + allowFontScaling={false} + style={ + Array [ + Object { + "color": "#5C6BC0", + "fontSize": 25, + }, + undefined, + Object { + "fontFamily": "material-community", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } +> +  </Text> `; diff --git a/src/Navigator/indexBlack.jsx b/src/Navigator/indexBlack.jsx new file mode 100644 index 0000000..725a78a --- /dev/null +++ b/src/Navigator/indexBlack.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { createBottomTabNavigator } from 'react-navigation-tabs'; +import TabBarIcon from './TabBarIcon'; +import Trans from '../Stats/index'; +import Chart from '../Chart/index'; +import TransCreator from '../TransCreator/index'; +import FamilyTrans from '../FamilyTrans/index'; +import PersonalNavigation from '../Personal/personalStackNavigation'; + +const AppNavigator = createBottomTabNavigator({ + Trans, + Chart, + Create: TransCreator, + Family: FamilyTrans, + Me: PersonalNavigation, +}, +{ + initialRouteName: 'Trans', // set default page + defaultNavigationOptions: ({ navigation }) => ({ + // eslint-disable-next-line react/prop-types + tabBarIcon: ({ focused, tintColor }) => ( + <TabBarIcon + navigation={navigation} + focused={focused} + tintColor={tintColor} + /> + ), + + }), + tabBarOptions: { + activeTintColor: '#5C6BC0', + inactiveTintColor: 'gray', + style: { + backgroundColor: '#181a35', + }, + }, +}); + +export default AppNavigator; diff --git a/src/Navigator/indexBlackProfile.jsx b/src/Navigator/indexBlackProfile.jsx new file mode 100644 index 0000000..7e1a812 --- /dev/null +++ b/src/Navigator/indexBlackProfile.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { createBottomTabNavigator } from 'react-navigation-tabs'; +import TabBarIcon from './TabBarIcon'; +import Trans from '../Stats/index'; +import Chart from '../Chart/index'; +import TransCreator from '../TransCreator/index'; +import FamilyTrans from '../FamilyTrans/index'; +import PersonalNavigation from '../Personal/personalStackNavigation'; + +const AppNavigator = createBottomTabNavigator({ + Trans, + Chart, + Create: TransCreator, + Family: FamilyTrans, + Me: PersonalNavigation, +}, +{ + initialRouteName: 'Me', // set default page + defaultNavigationOptions: ({ navigation }) => ({ + // eslint-disable-next-line react/prop-types + tabBarIcon: ({ focused, tintColor }) => ( + <TabBarIcon + navigation={navigation} + focused={focused} + tintColor={tintColor} + /> + ), + + }), + tabBarOptions: { + activeTintColor: '#5C6BC0', + inactiveTintColor: 'gray', + style: { + backgroundColor: '#181a35', + }, + }, +}); + +export default AppNavigator; diff --git a/src/Navigator/indexWhite.jsx b/src/Navigator/indexWhite.jsx new file mode 100644 index 0000000..57048b8 --- /dev/null +++ b/src/Navigator/indexWhite.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { createBottomTabNavigator } from 'react-navigation-tabs'; +import TabBarIcon from './TabBarIcon'; +import Trans from '../Stats/index'; +import Chart from '../Chart/index'; +import TransCreator from '../TransCreator/index'; +import FamilyTrans from '../FamilyTrans/index'; +import PersonalNavigation from '../Personal/personalStackNavigation'; + +const AppNavigator = createBottomTabNavigator({ + Trans, + Chart, + Create: TransCreator, + Family: FamilyTrans, + Me: PersonalNavigation, +}, +{ + initialRouteName: 'Trans', // set default page + defaultNavigationOptions: ({ navigation }) => ({ + // eslint-disable-next-line react/prop-types + tabBarIcon: ({ focused, tintColor }) => ( + <TabBarIcon + navigation={navigation} + focused={focused} + tintColor={tintColor} + /> + ), + + }), + tabBarOptions: { + activeTintColor: '#5C6BC0', + inactiveTintColor: 'gray', + style: { + backgroundColor: 'white', + }, + }, +}); + +export default AppNavigator; diff --git a/src/Navigator/index.jsx b/src/Navigator/indexWhiteProfile.jsx similarity index 56% rename from src/Navigator/index.jsx rename to src/Navigator/indexWhiteProfile.jsx index d00cd9c..c4a57f9 100644 --- a/src/Navigator/index.jsx +++ b/src/Navigator/indexWhiteProfile.jsx @@ -1,17 +1,21 @@ import React from 'react'; import { createBottomTabNavigator } from 'react-navigation-tabs'; import TabBarIcon from './TabBarIcon'; -import Stats from '../Stats/index'; +import Trans from '../Stats/index'; +import Chart from '../Chart/index'; import TransCreator from '../TransCreator/index'; -import Personal from '../Personal/index'; -import themeColor from '../Common/Color'; +import FamilyTrans from '../FamilyTrans/index'; +import PersonalNavigation from '../Personal/personalStackNavigation'; const AppNavigator = createBottomTabNavigator({ - Stats, + Trans, + Chart, Create: TransCreator, - Me: Personal, + Family: FamilyTrans, + Me: PersonalNavigation, }, { + initialRouteName: 'Me', // set default page defaultNavigationOptions: ({ navigation }) => ({ // eslint-disable-next-line react/prop-types tabBarIcon: ({ focused, tintColor }) => ( @@ -23,7 +27,13 @@ const AppNavigator = createBottomTabNavigator({ ), }), - tabBarOptions: themeColor.navColor, + tabBarOptions: { + activeTintColor: '#5C6BC0', + inactiveTintColor: 'gray', + style: { + backgroundColor: 'white', + }, + }, }); export default AppNavigator; diff --git a/src/Personal/Login.jsx b/src/Personal/Login.jsx new file mode 100644 index 0000000..3dce573 --- /dev/null +++ b/src/Personal/Login.jsx @@ -0,0 +1,118 @@ +import React, { useState, useEffect } from 'react'; +import { + View, Text, Dimensions, KeyboardAvoidingView, ImageBackground, Image, +} from 'react-native'; +import { Input, Icon } from 'react-native-elements'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigation } from 'react-navigation-hooks'; +import { requestLogin } from './actionCreator'; +import utils from './utils'; +import LoginView from './loginView'; +import SubmitBtn from './SubmitBtn'; + +const icon = require('../../assets/loginIcon.png'); +const bgImg = require('../../assets/loginBg.png'); + +export default function Login() { + const { navigate } = useNavigation(); + const user = useSelector((state) => state.user); + const dispatch = useDispatch(); + const screenWidth = Dimensions.get('window').width; + const screenHeight = Dimensions.get('window').height; + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [passwordHidden, setPasswordHidden] = useState(true); + const [errorMsgDisplay, setErrorMsgDisplay] = useState(false); + + const login = (userInfo) => { + dispatch(requestLogin(userInfo)); + }; + + useEffect(() => { + if (user.accessToken) navigate('Index'); + // dispatch(requestRestore()); + }, [user.accessToken]); + + useEffect(() => { + if (user.status !== '' && user.status !== undefined) { + setErrorMsgDisplay(true); + } + return () => { + setErrorMsgDisplay(false); + }; + }, [user.status]); + + return ( + <ImageBackground source={bgImg} style={{ width: screenWidth, height: screenHeight }}> + <KeyboardAvoidingView + style={LoginView.registerForm} + behavior="padding" + enabled + > + <View style={LoginView.icon}> + <Image source={icon} style={{ width: 220, height: 220 }} /> + </View> + <View style={LoginView.inputSection}> + <Input + placeholder="example@email.com" + leftIcon={( + <Icon + name="email" + color="#a8a8a8" + /> + )} + leftIconContainerStyle={LoginView.inputIcon} + label="Email Address" + textContentType="emailAddress" + value={email} + autoCapitalize="none" + onChangeText={(value) => setEmail(value)} + labelStyle={{ color: 'gray' }} + /> + <Text style={LoginView.note}> + {utils.emailValidation(email) || email === '' ? '' : 'Your email is not correct.'} + </Text> + </View> + <View style={LoginView.inputSection}> + <Input + placeholder="Password" + leftIcon={( + <Icon + name="lock" + color="#a8a8a8" + /> + )} + leftIconContainerStyle={LoginView.inputIcon} + label="Password" + secureTextEntry={passwordHidden} + value={password} + onChangeText={(value) => setPassword(value)} + labelStyle={{ color: 'gray' }} + rightIcon={( + <Icon + name={`md-eye${passwordHidden ? '-off' : ''}`} + type="ionicon" + color="gray" + onPress={() => setPasswordHidden(!passwordHidden)} + /> + )} + /> + </View> + { + (errorMsgDisplay) ? ( + <View style={[LoginView.inputSection, LoginView.errorBox]}> + <Text style={LoginView.errorText}>{user.message}</Text> + </View> + ) : (null) + } + <View style={{ paddingTop: 20 }}> + <SubmitBtn + disabled={utils.loginValidation(email, password) === false} + onPressBtn={() => login({ email, password })} + title="Login" + /> + </View> + </KeyboardAvoidingView> + </ImageBackground> + ); +} diff --git a/src/Personal/Personal.jsx b/src/Personal/Personal.jsx new file mode 100644 index 0000000..fff987c --- /dev/null +++ b/src/Personal/Personal.jsx @@ -0,0 +1,175 @@ +import React, { useState, useEffect } from 'react'; +import { + View, Text, ActivityIndicator, ScrollView, ImageBackground, +} from 'react-native'; +import { + Image, ListItem, Button, +} from 'react-native-elements'; +import { useDispatch, useSelector } from 'react-redux'; +import moment from 'moment'; +import { useNavigation } from 'react-navigation-hooks'; +import { requestBackup, requestRestore, logoutSuccessful } from './actionCreator'; +import MainHeader from '../Common/MainHeader'; +import RestoreOverlay from './RestoreOverlay'; +import ThemeSelection from './themeSelection/ThemeSelection'; +import setThemeStyle from '../Common/theme/setThemeStyle'; +import profileImg from '../../assets/profile.jpg'; + +const avatar = require('../../assets/avatar.png'); + +export default function Personal() { + const { navigate } = useNavigation(); + const dispatch = useDispatch(); + const backupState = useSelector((state) => state.backupState); + const restoreState = useSelector((state) => state.restoreState); + const user = useSelector((state) => state.user); + const { themeMode } = useSelector((state) => state.theme); + const styles = setThemeStyle(themeMode); + + const handleLogout = () => { + // dispatch(requestBackup()); + dispatch(logoutSuccessful()); + }; + + const [message, setMessage] = useState('Click to back up data'); + const [restoreMsg, setRestoreMsg] = useState('Click to restore data'); + const [isOverlayVisible, setOverlayVisibility] = useState(false); + + useEffect(() => { + if (backupState.error !== '') { + setMessage(backupState.error); + } + if (backupState.lastBackupDate !== null) { + setMessage(`Last update: ${moment.unix(backupState.lastBackupDate).format('YYYY-MM-DD HH:mm')}`); + } + if (restoreState.lastRestoreDate !== null) { + setRestoreMsg(`Last restore: ${moment.unix(restoreState.lastRestoreDate).format('YYYY-MM-DD HH:mm')}`); + } + if (restoreState.error !== '') { + setRestoreMsg(restoreState.error); + } + }, [ + backupState.lastBackupDate, + backupState.error, + restoreState.lastRestoreDate, + restoreState.error]); + + const restoreData = () => { + dispatch(requestRestore()); + setOverlayVisibility(false); + }; + + return ( + <View style={{ flex: 1, flexDirection: 'column' }}> + <MainHeader title="Profile" /> + <ScrollView style={[styles.deviceBody, { flex: 1, flexDirection: 'column' }]}> + <RestoreOverlay + isVisible={isOverlayVisible} + onConfirm={() => restoreData()} + onCancel={() => setOverlayVisibility(false)} + /> + <View style={[styles.card, { padding: 0, overflow: 'hidden' }]}> + <ImageBackground + source={profileImg} + style={{ borderTopLeftRadius: 6, borderTopRightRadius: 6 }} + > + <View style={{ + alignItems: 'center', paddingVertical: 30, + }} + > + <Image source={avatar} style={{ width: 85, height: 85 }} /> + <Text style={[styles.secondaryHeading, { color: '#FAFAFA', fontWeight: 'bold', fontSize: 30 }]}>{user.username}</Text> + <Text style={{ color: '#FAFAFA', fontWeight: 'bold', fontSize: 15 }}>{user.email}</Text> + </View> + </ImageBackground> + <View style={{ paddingTop: 15, paddingBottom: 10, paddingHorizontal: 10 }}> + <Text + style={[styles.secondaryHeading, { marginLeft: 12, marginBottom: 5 }]} + > + General + </Text> + <ListItem + title="Personal details" + titleStyle={styles.listHeading} + subtitle="you can edit your information about your email address, phone number" + subtitleStyle={{ color: 'grey' }} + bottomDivider + containerStyle={{ backgroundColor: 'transparent' }} + /> + <ListItem + title="Backup data" + titleStyle={styles.listHeading} + subtitle={message} + subtitleStyle={{ color: 'grey' }} + rightElement={backupState.isInProgress ? <ActivityIndicator size="small" color="grey" /> + : ( + <Button + title="Backup" + titleStyle={{ color: '#2fc899' }} + type="outline" + buttonStyle={{ borderColor: '#2fc899', borderRadius: 6 }} + onPress={() => dispatch(requestBackup())} + /> + )} + containerStyle={{ backgroundColor: 'rgba(255, 255, 255, 0)' }} + bottomDivider + /> + <ListItem + title="My family" + titleStyle={styles.listHeading} + containerStyle={{ backgroundColor: 'transparent' }} + onPress={() => navigate('FamilyPage')} + bottomDivider + /> + <ListItem + title="Change password" + titleStyle={styles.listHeading} + containerStyle={{ backgroundColor: 'rgba(255, 255, 255, 0)' }} + bottomDivider + /> + <ListItem + title="Theme" + titleStyle={styles.listHeading} + rightElement={( + <ThemeSelection /> + )} + containerStyle={{ backgroundColor: 'rgba(255, 255, 255, 0)' }} + /> + </View> + </View> + <View style={styles.card}> + <Text style={[styles.secondaryHeading, { marginLeft: 12, marginBottom: 5 }]}> + Cloud Service + </Text> + <ListItem + title="Restore data" + titleStyle={styles.listHeading} + subtitle={restoreMsg} + subtitleStyle={{ color: 'grey' }} + rightElement={restoreState.isInProgress ? <ActivityIndicator size="small" color="grey" /> + : ( + <Button + title="Restore" + titleStyle={{ color: '#2fc899' }} + type="outline" + buttonStyle={{ borderColor: '#2fc899', borderRadius: 6 }} + onPress={() => setOverlayVisibility(true)} + /> + )} + bottomDivider + containerStyle={{ backgroundColor: 'rgba(255, 255, 255, 0)' }} + /> + <ListItem + title="Banking Sync" + titleStyle={styles.listHeading} + subtitle="Sync your transactions from bank" + subtitleStyle={{ color: 'grey' }} + onPress={() => navigate('OpenBanking')} + containerStyle={{ backgroundColor: 'rgba(255, 255, 255, 0)' }} + /> + <Button title="Logout" color="#f194ff" onPress={handleLogout} /> + </View> + </ScrollView> + </View> + ); +} diff --git a/src/Personal/Register.jsx b/src/Personal/Register.jsx new file mode 100644 index 0000000..8f44cd1 --- /dev/null +++ b/src/Personal/Register.jsx @@ -0,0 +1,168 @@ +import React, { useState, useEffect } from 'react'; +import { + View, Text, Dimensions, KeyboardAvoidingView, ImageBackground, Image, +} from 'react-native'; +import { Input, Icon } from 'react-native-elements'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigation } from 'react-navigation-hooks'; +import utils from './utils'; +import RegisterView from './registerViewStyle'; +import { requestSignup } from './actionCreator'; +import SubmitBtn from './SubmitBtn'; + +const icon = require('../../assets/loginIcon.png'); +const bgImg = require('../../assets/loginBg.png'); + +export default function Register() { + const { navigate } = useNavigation(); + const user = useSelector((state) => state.user); + const dispatch = useDispatch(); + const screenWidth = Dimensions.get('window').width; + const screenHeight = Dimensions.get('window').height; + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordHidden, setPasswordHidden] = useState(true); + const [confirmPasswordHidden, setConfirmPasswordHidden] = useState(true); + const [errorMsgDisplay, setErrorMsgDisplay] = useState(false); + + const signUp = (userInfo) => { + dispatch(requestSignup(userInfo)); + }; + + useEffect(() => { + if (user.accessToken) navigate('Index'); + }, [user.accessToken]); + + useEffect(() => { + if (user.status !== '' && user.status !== undefined) { + setErrorMsgDisplay(true); + } + }, [user.status]); + + return ( + <ImageBackground source={bgImg} style={{ width: screenWidth, height: screenHeight }}> + <KeyboardAvoidingView + style={RegisterView.registerForm} + behavior="padding" + keyboardVerticalOffset={300} + enabled + > + <View style={RegisterView.icon}> + <Image source={icon} style={{ width: 220, height: 220 }} /> + </View> + <View style={RegisterView.inputSection}> + <Input + placeholder="username" + leftIcon={( + <Icon + name="face" + color="#a8a8a8" + /> + )} + leftIconContainerStyle={RegisterView.inputIcon} + label="Username" + value={username} + onChangeText={(value) => setUsername(value)} + labelStyle={{ color: 'gray' }} + /> + </View> + <View style={RegisterView.inputSection}> + <Input + placeholder="example@email.com" + leftIcon={( + <Icon + name="email" + color="#a8a8a8" + /> + )} + leftIconContainerStyle={RegisterView.inputIcon} + label="Email Address" + autoCapitalize="none" + textContentType="emailAddress" + value={email} + onChangeText={(value) => setEmail(value)} + labelStyle={{ color: 'gray' }} + /> + <Text style={RegisterView.note}> + {utils.emailValidation(email) || email === '' ? '' : 'Your email is not correct.'} + </Text> + </View> + + <View style={RegisterView.inputSection}> + <Input + placeholder="Password" + leftIcon={( + <Icon + name="lock" + color="#a8a8a8" + /> + )} + leftIconContainerStyle={RegisterView.inputIcon} + label="Password" + secureTextEntry={passwordHidden} + value={password} + onChangeText={(value) => setPassword(value)} + labelStyle={{ color: 'gray' }} + rightIcon={( + <Icon + name={`md-eye${passwordHidden ? '-off' : ''}`} + type="ionicon" + color="gray" + onPress={() => setPasswordHidden(!passwordHidden)} + /> + )} + /> + <Text style={RegisterView.note}> + {utils.passwordValidation(password) || password === '' + ? '' : 'Password should be at least 8 characters.'} + </Text> + </View> + + <View style={RegisterView.inputSection}> + <Input + placeholder="Confirm your password" + leftIcon={( + <Icon + name="lock" + color="#a8a8a8" + /> + )} + leftIconContainerStyle={RegisterView.inputIcon} + label="Confirm your password" + secureTextEntry={confirmPasswordHidden} + value={confirmPassword} + onChangeText={(value) => setConfirmPassword(value)} + labelStyle={{ color: 'gray' }} + rightIcon={( + <Icon + name={`md-eye${confirmPasswordHidden ? '-off' : ''}`} + type="ionicon" + color="gray" + onPress={() => setConfirmPasswordHidden(!confirmPasswordHidden)} + /> + )} + /> + <Text style={RegisterView.note}> + {password === confirmPassword || confirmPassword === '' ? '' : 'Password is not the same.'} + </Text> + </View> + { + (errorMsgDisplay) ? ( + <View style={[RegisterView.inputSection, RegisterView.errorBox]}> + <Text style={RegisterView.errorText}>{user.message}</Text> + </View> + ) : (null) + } + <View> + <SubmitBtn + disabled={utils.validateSignup(username, password, confirmPassword, email) === false} + onPressBtn={() => signUp({ email, password, username })} + title="Register" + /> + </View> + </KeyboardAvoidingView> + </ImageBackground> + ); +} diff --git a/src/Personal/RestoreOverlay.jsx b/src/Personal/RestoreOverlay.jsx new file mode 100644 index 0000000..d14981c --- /dev/null +++ b/src/Personal/RestoreOverlay.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { View, Text } from 'react-native'; +import { Button, Overlay } from 'react-native-elements'; +import { LinearGradient } from 'expo-linear-gradient'; +import PropTypes from 'prop-types'; +import styles from '../Common/themeStyleLight'; +import themeColor from '../Common/Color'; + +export default function RestoreOverlay({ isVisible, onConfirm, onCancel }) { + return ( + <Overlay height={220} isVisible={isVisible} overlayStyle={{ padding: 0 }}> + <View style={{ flex: 1, flexDirection: 'column', justifyContent: 'space-between' }}> + <LinearGradient + colors={themeColor.gradientColor.green} + start={[0.1, 0.9]} + end={[0.9, 0.1]} + style={{ + paddingTop: 35, + paddingBottom: 35, + paddingLeft: 20, + paddingRight: 20, + }} + > + <Text + style={styles.overlayText} + > + Please make sure you have backup all your transactions + </Text> + </LinearGradient> + <View style={{ flexDirection: 'row', justifyContent: 'space-around' }}> + <View style={[styles.greenLargeBtn, { marginTop: 0, marginBottom: 30 }]}> + <LinearGradient + colors={themeColor.gradientColor.green} + start={[0.1, 0.9]} + end={[0.9, 0.1]} + style={styles.overlayBtn} + > + <Button title="Confirm" type="clear" onPress={() => onConfirm()} titleStyle={{ color: 'white' }} /> + </LinearGradient> + </View> + <View style={[styles.grayBtnShadow, { marginBottom: 30 }]}> + <LinearGradient + colors={['#ffff', '#ffff']} + style={styles.overlayBtn} + > + <Button title="Cancel" type="clear" onPress={() => onCancel()} titleStyle={{ color: 'grey' }} /> + </LinearGradient> + </View> + </View> + </View> + </Overlay> + ); +} + +RestoreOverlay.propTypes = { + isVisible: PropTypes.bool.isRequired, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +}; diff --git a/src/Personal/SubmitBtn.jsx b/src/Personal/SubmitBtn.jsx new file mode 100644 index 0000000..49d0bdf --- /dev/null +++ b/src/Personal/SubmitBtn.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { View } from 'react-native'; +import { Button } from 'react-native-elements'; +import { LinearGradient } from 'expo-linear-gradient'; +import PropTypes from 'prop-types'; +import colors from '../Common/Color'; +import LoginView from './loginView'; + +export default function SubmitBtn({ disabled, onPressBtn, title }) { + return ( + <View style={disabled ? LoginView.disabledBtnShadow : LoginView.btnShadow}> + <LinearGradient + colors={disabled ? colors.gradientColor.gray : colors.gradientColor.green} + start={[0.1, 0.9]} + end={[0.9, 0.1]} + style={{ borderRadius: 30, padding: 10 }} + > + <Button + title={title} + disabledTitleStyle={{ color: 'white' }} + titleStyle={{ color: 'white' }} + type="clear" + disabled={disabled} + onPress={onPressBtn} + /> + </LinearGradient> + </View> + ); +} + +SubmitBtn.propTypes = { + disabled: PropTypes.bool.isRequired, + onPressBtn: PropTypes.func.isRequired, + title: PropTypes.string.isRequired, +}; diff --git a/src/Personal/actionCreator.js b/src/Personal/actionCreator.js new file mode 100644 index 0000000..29963ce --- /dev/null +++ b/src/Personal/actionCreator.js @@ -0,0 +1,231 @@ +import { BACKEND_URL } from 'react-native-dotenv'; +import { restoreTransactions } from '../Stats/actionCreator'; + +// const BACKEND_URL = 'http://10.72.160.173:8080'; +const moment = require('moment'); + +export const actionType = { + BACKUP_START: 'BACKUP_START', + BACKUP_SUCCESSFUL: 'BACKUP_SUCCESSFUL', + BACKUP_FAILED: 'BACKUP_FAILED', + SIGNUP_START: 'SIGNUP_START', + SIGNUP_FAILED: 'SIGNUP_FAILED', + SIGNUP_SUCCESSFUL: 'SIGNUP_SUCCESSFUL', + LOGOUT_SUCCESSFUL: 'LOGOUT_SUCCESSFUL', + LOGIN_START: 'LOGIN_START', + LOGIN_FAILED: 'LOGIN_FAILED', + LOGIN_SUCCESSFUL: 'LOGIN_SUCCESSFUL', + RESTORE_START: 'RESTORE_START', + RESTORE_SUCCESSFUL: 'RESTORE_SUCCESSFUL', + RESTORE_FAILED: 'RESTORE_FAILED', +}; + +export function backupStart() { + return { + type: actionType.BACKUP_START, + }; +} + +export function backupSuccessful(timestamp) { + return { + type: actionType.BACKUP_SUCCESSFUL, + payload: timestamp, + }; +} + +export function backupFailed(errorMsg) { + return { + type: actionType.BACKUP_FAILED, + payload: errorMsg, + }; +} + +export function signupStart() { + return { + type: actionType.SIGNUP_START, + }; +} + +export function signupSuccessful(userInfo) { + return { + type: actionType.SIGNUP_SUCCESSFUL, + payload: userInfo, + }; +} + +export function restoreStart() { + return { + type: actionType.RESTORE_START, + }; +} + +export function restoreSuccessful(payload) { + return { + type: actionType.RESTORE_SUCCESSFUL, + payload, + }; +} + +export function restoreFailed(payload) { + return { + type: actionType.RESTORE_FAILED, + payload, + }; +} + +export function loginStart() { + return { + type: actionType.LOGIN_START, + }; +} + +export function loginSuccessful(userInfo) { + return { + type: actionType.LOGIN_SUCCESSFUL, + payload: userInfo, + }; +} + +export function loginFailed(userInfo) { + return { + type: actionType.LOGIN_FAILED, + payload: userInfo, + }; +} + +export const requestBackup = () => (dispatch, getState) => { + const { transactions } = getState().transactions; + const { accessToken } = getState().user; + + if (!accessToken) { + dispatch(backupFailed('Unauthorized token')); + return; + } + dispatch(backupStart()); + fetch(`${BACKEND_URL}/backup`, { + method: 'POST', + mode: 'cors', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ transactions }), + }).then((response) => { + if (response.status === 200) { + setTimeout(() => dispatch(backupSuccessful(moment().unix())), 300); + throw new Error(200); + } else { + return response.json(); + } + }).then((response) => { + setTimeout(() => dispatch(backupFailed(response.message)), 300); + }).catch((error) => { + if (error.message !== '200') { + dispatch(backupFailed('Network error, Please try again later')); + } + }); +}; + +export const requestRestore = () => (dispatch, getState) => { + const { accessToken } = getState().user; + if (!accessToken) { + dispatch(restoreFailed('Unauthorized token')); + return; + } + dispatch(restoreStart()); + + fetch(`${BACKEND_URL}/backup`, { + method: 'GET', + mode: 'cors', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }).then((response) => { + console.log(response); + if (response.status === 401) { + dispatch(restoreFailed(response.message)); + throw new Error(response.status); + } else { + return response.json(); + } + }).then((response) => { + if (response.code) { + dispatch(restoreFailed(response.message)); + } else { + dispatch(restoreTransactions(response)); + setTimeout(() => dispatch(restoreSuccessful(moment().unix())), 300); + } + }).catch((error) => { + if (error !== '401') { + dispatch(restoreFailed('Network error, Please try again later')); + } + }); +}; + +export function signupFailed(userInfo) { + return { + type: actionType.SIGNUP_FAILED, + payload: userInfo, + }; +} + +export function logoutSuccessful() { + return { + type: actionType.LOGOUT_SUCCESSFUL, + }; +} + +export const requestSignup = (userInfo) => (dispatch) => { + dispatch(signupStart()); + fetch(`${BACKEND_URL}/register`, { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'Application/json', + }, + body: JSON.stringify(userInfo), + }).then((response) => response.json()).then((response) => { + if (response.accessToken !== '' && response.accessToken !== undefined) { + dispatch( + signupSuccessful({ + accessToken: response.accessToken, + email: userInfo.email, + password: userInfo.password, + username: userInfo.username, + }), + ); + } else { + dispatch( + signupFailed({ status: response.code, message: response.message }), + ); + } + }) + .catch(() => { + dispatch(signupFailed()); + }); +}; + +export const requestLogin = (userInfo) => (dispatch) => { + dispatch(loginStart()); + fetch(`${BACKEND_URL}/sessions`, { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'Application/json', + }, + body: JSON.stringify(userInfo), + }).then((response) => response.json()).then((response) => { + if (response.accessToken !== '' && response.accessToken !== undefined) { + dispatch(loginSuccessful({ + email: userInfo.email, + accessToken: response.accessToken, + username: response.username, + })); + } else { + dispatch(loginFailed({ status: response.code, message: response.message })); + } + }).catch((err) => { + dispatch(loginFailed({ status: null, message: err.message })); + }); +}; diff --git a/src/Personal/backupReducer.js b/src/Personal/backupReducer.js new file mode 100644 index 0000000..4e37a90 --- /dev/null +++ b/src/Personal/backupReducer.js @@ -0,0 +1,36 @@ +import { actionType } from './actionCreator'; + +const initialBackup = { + isInProgress: false, + error: '', + lastBackupDate: null, +}; + +const backupState = (state = initialBackup, action) => { + switch (action.type) { + case actionType.BACKUP_START: + return { + ...state, + isInProgress: true, + error: '', + }; + case actionType.BACKUP_SUCCESSFUL: + return { + ...state, + isInProgress: false, + lastBackupDate: action.payload, + error: '', + }; + case actionType.BACKUP_FAILED: + return { + ...state, + isInProgress: false, + error: action.payload, + lastBackupDate: null, + }; + default: + return state; + } +}; + +export default backupState; diff --git a/src/Personal/familyGroup/AddMemberOverlay.jsx b/src/Personal/familyGroup/AddMemberOverlay.jsx new file mode 100644 index 0000000..396da14 --- /dev/null +++ b/src/Personal/familyGroup/AddMemberOverlay.jsx @@ -0,0 +1,51 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + View, Dimensions, Platform, ScrollView, +} from 'react-native'; +import { Overlay, SearchBar, ListItem } from 'react-native-elements'; + +import PropTypes from 'prop-types'; +import { searchMember } from './actionCreator'; + +export default function AddMemberOverlay({ isVisible, onConfirm, onCancel }) { + const { members } = useSelector((state) => state.searchedList); + const dispatch = useDispatch(); + const screenWidth = Dimensions.get('window').width; + const [searchText, setSearchText] = useState(''); + const searchMemberByName = (value) => { + setSearchText(value); + dispatch(searchMember(value)); + }; + + + return ( + <Overlay + height={70} + width={screenWidth} + overlayStyle={{ position: 'absolute', top: 0, padding: 0 }} + isVisible={isVisible} + > + <View> + <SearchBar + placeholder="Search member" + platform="ios" + onChangeText={(value) => searchMemberByName(value)} + value={searchText} + autoCapitalize="none" + containerStyle={{ width: screenWidth, marginTop: (Platform.OS === 'ios' ? 35 : 0) }} + onCancel={onCancel} + /> + <ScrollView style={{ zIndex: 999, height: 500 }}> + {members.map((item, idx) => <ListItem key={`ListGroup:${idx + 1}`} title={item.username} onPress={() => onConfirm(item)} bottomDivider />)} + </ScrollView> + </View> + </Overlay> + ); +} + +AddMemberOverlay.propTypes = { + isVisible: PropTypes.bool.isRequired, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +}; diff --git a/src/Personal/familyGroup/FamilyCard.jsx b/src/Personal/familyGroup/FamilyCard.jsx new file mode 100644 index 0000000..440cf37 --- /dev/null +++ b/src/Personal/familyGroup/FamilyCard.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { View } from 'react-native'; +import { useSelector } from 'react-redux'; +import { ListItem } from 'react-native-elements'; +import PropTypes from 'prop-types'; +import setThemeStyle from '../../Common/theme/setThemeStyle'; + +export default function FamilyCard({ username, note, imgUrl }) { + const { themeMode } = useSelector((state) => state.theme); + const styles = setThemeStyle(themeMode); + return ( + <View style={[styles.card, { padding: 0, overflow: 'hidden' }]}> + <ListItem + leftAvatar={{ source: { uri: imgUrl } }} + title={username} + titleStyle={styles.listHeading} + subtitle={note} + subtitleStyle={{ color: 'grey' }} + containerStyle={{ backgroundColor: 'rgba(255, 255, 255, 0)' }} + /> + </View> + ); +} + + +FamilyCard.propTypes = { + username: PropTypes.string, + note: PropTypes.string, + imgUrl: PropTypes.string, +}; + +FamilyCard.defaultProps = { + username: '', + note: '', + imgUrl: '', +}; diff --git a/src/Personal/familyGroup/actionCreator.js b/src/Personal/familyGroup/actionCreator.js new file mode 100644 index 0000000..d575e53 --- /dev/null +++ b/src/Personal/familyGroup/actionCreator.js @@ -0,0 +1,134 @@ +import { BACKEND_URL } from 'react-native-dotenv'; +import { getFamilyTransactions } from '../../FamilyTrans/actionCreator'; + +// const BACKEND_URL = 'http://10.72.160.173:8080'; + +export const actionType = { + GET_MEMBER_LIST: 'GET_MEMBER_LIST', + SEARCH_FAILED: 'SEARCH_FAILED', + GET_FAMILY_MEMBER: 'GET_FAMILY_MEMBER', + GET_FAMILY_MEMBER_FAILED: 'GET_FAMILY_MEMBER_FAILED', + CONFIRM_FAMILY_MEMBER: 'CONFIRM_FAMILY_MEMBER', + CANCEL_MEMBER_SEARCH: 'CANCEL_MEMBER_SEARCH', + UPDATE_FAMILY_LIST_FAILED: 'UPDATE_FAMILY_LIST_FAILED', +}; + +export function getMemberList(payload) { + return { + type: actionType.GET_MEMBER_LIST, + payload, + }; +} + +export function searchFailed(payload) { + return { + type: actionType.SEARCH_FAILED, + payload, + }; +} + +export function cancelMemberSearch() { + return { + type: actionType.CANCEL_MEMBER_SEARCH, + }; +} + +export function updateFamilyListFailed(error) { + return { + type: actionType.UPDATE_FAMILY_LIST_FAILED, + error, + }; +} + +export function confirmFamilyMember(payload) { + return { + type: actionType.CONFIRM_FAMILY_MEMBER, + payload, + }; +} + +export function getFamilyMember(payload) { + return { + type: actionType.GET_FAMILY_MEMBER, + payload, + }; +} + +export function getFamilyMemberFailed(error) { + return { + type: actionType.GET_FAMILY_MEMBER_FAILED, + error, + }; +} + +export const searchMember = (keyword) => (dispatch, getState) => { + const { accessToken } = getState().user; + fetch(`${BACKEND_URL}/users?contain=${keyword}`, { + method: 'GET', + mode: 'cors', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }).then((response) => { + if (response.status === 401) { + dispatch(searchFailed(response.message)); + throw new Error(response.status); + } else { + return response.json(); + } + }).then((response) => { + dispatch(getMemberList(response)); + }).catch(() => { + dispatch(searchFailed('Network error, Please try again later')); + }); +}; + +export const updateFamilyMember = () => (dispatch, getState) => { + const { members } = getState().familyList; + const { accessToken } = getState().user; + const membersList = members.map((item) => item.username); + fetch(`${BACKEND_URL}/family`, { + method: 'POST', + mode: 'cors', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ members: membersList }), + }).then((response) => { + if (response.status !== 200) { + return response.json(); + } + dispatch(getFamilyTransactions()); + }).then((response) => { + if (response === undefined) { + dispatch(updateFamilyListFailed('Unauthorized error')); + } else { + dispatch(updateFamilyListFailed(response.message)); + } + }).catch(() => { + dispatch(updateFamilyListFailed('Network Error, Please try again later')); + }); +}; + +export const getFamilyMemberList = () => (dispatch, getState) => { + const { accessToken } = getState().user; + fetch(`${BACKEND_URL}/family-members`, { + method: 'GET', + mode: 'cors', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }).then((response) => response.json()) + .then((response) => { + if (response.code === 200) { + dispatch(getFamilyMember(response.data)); + } else { + dispatch(getFamilyMemberFailed(response.message)); + } + }).catch(() => { + dispatch(getFamilyMemberFailed('Network Error, Please try again later')); + }); +}; diff --git a/src/Personal/familyGroup/familyReducer.js b/src/Personal/familyGroup/familyReducer.js new file mode 100644 index 0000000..4f634a7 --- /dev/null +++ b/src/Personal/familyGroup/familyReducer.js @@ -0,0 +1,28 @@ +import { actionType } from './actionCreator'; + +const initialMemberList = { + members: [], + error: '', +}; + +export default (state = initialMemberList, action) => { + switch (action.type) { + case actionType.CONFIRM_FAMILY_MEMBER: + return { + ...state, + members: [...state.members, { ...action.payload }], + }; + case actionType.GET_FAMILY_MEMBER: + return { + ...state, + members: action.payload, + }; + case actionType.GET_FAMILY_MEMBER_FAILED: + return { + ...state, + error: '', + }; + default: + return state; + } +}; diff --git a/src/Personal/familyGroup/familySearchReducer.js b/src/Personal/familyGroup/familySearchReducer.js new file mode 100644 index 0000000..c3ddde1 --- /dev/null +++ b/src/Personal/familyGroup/familySearchReducer.js @@ -0,0 +1,34 @@ +import { actionType } from './actionCreator'; + +const initialSearchList = { + members: [], + error: '', +}; + +export default (state = initialSearchList, action) => { + switch (action.type) { + case actionType.GET_MEMBER_LIST: + return { + ...state, + members: action.payload, + }; + case actionType.SEARCH_FAILED: + return { + ...state, + error: '', + members: [], + }; + case actionType.CONFIRM_FAMILY_MEMBER: + return { + ...state, + members: [], + }; + case actionType.CANCEL_MEMBER_SEARCH: + return { + ...state, + members: [], + }; + default: + return state; + } +}; diff --git a/src/Personal/familyGroup/index.jsx b/src/Personal/familyGroup/index.jsx new file mode 100644 index 0000000..ea399d5 --- /dev/null +++ b/src/Personal/familyGroup/index.jsx @@ -0,0 +1,58 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { View, ScrollView, ImageBackground } from 'react-native'; +import setThemeStyle from '../../Common/theme/setThemeStyle'; +import MainHeader from '../../Common/MainHeader'; +import FamilyCard from './FamilyCard'; +import SubmitBtn from '../SubmitBtn'; +import AddMemberOverlay from './AddMemberOverlay'; +import { + cancelMemberSearch, updateFamilyMember, getFamilyMemberList, confirmFamilyMember, +} from './actionCreator'; + +const bgImg = require('../../../assets/familyView.png'); + +export default function FamilyPage() { + const { themeMode } = useSelector((state) => state.theme); + const { members } = useSelector((state) => state.familyList); + + const styles = setThemeStyle(themeMode); + const [isOverlayVisible, setOverlayVisibility] = useState(false); + const dispatch = useDispatch(); + const addMemberName = (value) => { + const searchIndex = members.findIndex((item) => item.id === value.id); + if (searchIndex === -1) { + dispatch(confirmFamilyMember(value)); + dispatch(updateFamilyMember()); + } + setOverlayVisibility(false); + }; + + const cancelSearch = () => { + dispatch(cancelMemberSearch()); + setOverlayVisibility(false); + }; + + useEffect(() => { + dispatch(getFamilyMemberList()); + }, ['']); + + return ( + <View style={{ flex: 1, flexDirection: 'column' }}> + <AddMemberOverlay + isVisible={isOverlayVisible} + onConfirm={(value) => addMemberName(value)} + onCancel={() => cancelSearch()} + /> + <MainHeader title="My Family" /> + <ImageBackground source={bgImg} imageStyle={{ opacity: 0.2 }} style={[styles.deviceBody, { flex: 1, flexDirection: 'column' }]}> + <ScrollView> + {members.map((value, idx) => <FamilyCard key={`ListGroup:${idx + 1}`} username={value.username} note="Father" imgUrl={value.url ? value.url : 'https://s3.amazonaws.com/uifaces/faces/twitter/ladylexy/128.jpg'} />)} + </ScrollView> + <View style={{ marginHorizontal: 10, marginBottom: 15 }}> + <SubmitBtn title="Add Member" disabled={false} onPressBtn={() => setOverlayVisibility(true)} /> + </View> + </ImageBackground> + </View> + ); +} diff --git a/src/Personal/index.jsx b/src/Personal/index.jsx index 355a416..b1d56fa 100644 --- a/src/Personal/index.jsx +++ b/src/Personal/index.jsx @@ -1,10 +1,63 @@ import React from 'react'; -import { View, Text } from 'react-native'; +import { View, Dimensions } from 'react-native'; +import { Button, Image } from 'react-native-elements'; +import { useNavigation } from 'react-navigation-hooks'; +import { useSelector } from 'react-redux'; +import { LinearGradient } from 'expo-linear-gradient'; +import RegisterView from './registerViewStyle'; +import Personal from './Personal'; +import colors from '../Common/Color'; -export default function Personal() { +const img = require('../../assets/personalDefaultPage.png'); + +export default function Index() { + const { navigate } = useNavigation(); + const user = useSelector((state) => state.user); + const screenWidth = Dimensions.get('window').width; + const screenHeight = Dimensions.get('window').height; return ( - <View> - <Text>Personal Screen</Text> - </View> + (user.accessToken !== '' && user.accessToken !== undefined) ? <Personal /> : ( + <> + <View style={{ flex: 1, paddingTop: 120 }}> + <Image + source={img} + style={{ marginTop: 50, width: screenWidth, height: screenHeight / 3 }} + /> + </View> + <View style={RegisterView.container}> + <View> + <LinearGradient + colors={colors.gradientColor.green} + start={[0.1, 0.9]} + end={[0.9, 0.1]} + style={{ + borderRadius: 30, paddingTop: 10, paddingBottom: 10, width: 140, + }} + > + <Button title="Login" type="clear" titleStyle={{ color: 'white' }} onPress={() => navigate('Login')} /> + </LinearGradient> + </View> + <View> + <LinearGradient + colors={colors.gradientColor.white} + start={[0.1, 0.9]} + end={[0.9, 0.1]} + style={{ + borderRadius: 30, paddingTop: 9, paddingBottom: 9, width: 140, borderColor: '#f6f6f6', borderWidth: 2, + }} + > + <Button + title="Sign up" + type="clear" + titleStyle={{ + color: 'gray', + }} + onPress={() => navigate('Register')} + /> + </LinearGradient> + </View> + </View> + </> + ) ); } diff --git a/src/Personal/loginView.js b/src/Personal/loginView.js new file mode 100644 index 0000000..26dcce3 --- /dev/null +++ b/src/Personal/loginView.js @@ -0,0 +1,84 @@ +import { StyleSheet } from 'react-native'; + +const btnShadow = { + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.70, + shadowRadius: 4.65, + elevation: 8, +}; + +const LoginView = StyleSheet.create({ + registerForm: { + alignContent: 'center', + paddingLeft: 10, + paddingRight: 10, + flex: 1, + paddingTop: 40, + marginBottom: 40, + flexDirection: 'column', + justifyContent: 'center', + }, + icon: { + alignItems: 'center', + paddingBottom: 5, + }, + inputIcon: { + marginLeft: 0, + marginRight: 5, + }, + inputSection: { + marginBottom: 20, + }, + button: { + alignSelf: 'stretch', + alignItems: 'center', + padding: 20, + backgroundColor: '#59cbbd', + marginTop: 30, + }, + buttonText: { + color: '#fff', + fontWeight: 'bold', + }, + note: { + color: 'red', + width: 'auto', + marginLeft: 10, + }, + container: { + flex: 1, + justifyContent: 'center', + backgroundColor: '#fff', + paddingLeft: 60, + paddingRight: 60, + }, + + errorBox: { + backgroundColor: '#f3dee0', + paddingTop: 10, + paddingBottom: 10, + marginLeft: 10, + marginRight: 10, + borderRadius: 5, + }, + + errorText: { + color: '#eb3d3d', + textAlign: 'center', + }, + + btnShadow: { + shadowColor: '#70ea93', + ...btnShadow, + }, + + disabledBtnShadow: { + shadowColor: '#ababab', + ...btnShadow, + }, +}); + +export default LoginView; diff --git a/src/Personal/openBanking/WebView.jsx b/src/Personal/openBanking/WebView.jsx new file mode 100644 index 0000000..02546c6 --- /dev/null +++ b/src/Personal/openBanking/WebView.jsx @@ -0,0 +1,5 @@ +import React from 'react'; +import { useNavigationParam } from 'react-navigation-hooks'; +import { WebView } from 'react-native-webview'; + +export default () => <WebView source={{ uri: useNavigationParam('url') }} style={{ marginTop: 50 }} />; diff --git a/src/Personal/openBanking/actionCreator.js b/src/Personal/openBanking/actionCreator.js new file mode 100644 index 0000000..0ca47cc --- /dev/null +++ b/src/Personal/openBanking/actionCreator.js @@ -0,0 +1,95 @@ +const basicToken = 'Basic ZTlhYzQ1YmItZGQ4Mi00NWEwLWEwOTItYzVhMWJmMzk4NTgxOmRjNmYwNzU3LTZmZDktNGFkOS1hY2M2LTI4NzgyZTI3YjNjNQ=='; + +export const actionType = { + SET_BANK_LIST: 'SET_BANK_LIST', + SET_BANK_ID_CODE: 'SET_BANK_ID_CODE', + SET_BANK_ACCESS_TOKEN: 'SET_BANK_ACCESS_TOKEN', + SET_ACCOUNT_LIST: 'SET_ACCOUNT_LIST', + SET_ACCOUNT_TRANSACTION: 'SET_ACCOUNT_TRANSACTION', +}; + +export const setBankList = (list) => ({ + type: actionType.SET_BANK_LIST, + payload: list, +}); + +export const setBankIDCode = (id, code) => ({ + type: actionType.SET_BANK_ID_CODE, + payload: { id, code }, +}); + +export const setAccountAccessToken = (token) => ({ + type: actionType.SET_BANK_ACCESS_TOKEN, + payload: token, +}); + +export const setAccountList = (list) => ({ + type: actionType.SET_ACCOUNT_LIST, + payload: list, +}); + +export const setTransactions = (list) => ({ + type: actionType.SET_ACCOUNT_TRANSACTION, + payload: list, +}); + +export const loadBankList = () => (dispatch) => { + fetch('https://oauth-sandbox.fintecture.com/res/v1/providers', { + headers: { + app_id: 'e9ac45bb-dd82-45a0-a092-c5a1bf398581', + Accept: 'application/json', + }, + }) + .then((response) => response.json()) + .then((response) => { dispatch(setBankList(response.data)); }); +}; + +const encodeFormData = (data) => Object.keys(data) + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`) + .join('&'); + +export const loadTransactions = (token, id, accountID) => (dispatch) => { + fetch(`https://api-sandbox.fintecture.com/ais/v1/customer/${id}/accounts/${accountID}/transactions`, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + }) + .then((response) => response.json()) + .then((response) => dispatch(setTransactions(response.data))); +}; + +export const loadAccountList = (token, account) => (dispatch) => { + fetch(`https://oauth-sandbox.fintecture.com/ais/v1/customer/${account}/accounts/`, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + }) + .then((response) => response.json()) + .then((response) => { + dispatch(setAccountList(response.data)); + dispatch(loadTransactions(token, account, response.data[0].id)); + }); +}; + +export const getAccessToken = (code, id) => (dispatch) => { + fetch('https://oauth-sandbox.fintecture.com/oauth/accesstoken', { + method: 'post', + headers: { + Accept: 'application/json', + Authorization: basicToken, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: encodeFormData({ + grant_type: 'authorization_code', + code, + scope: 'AIS', + }), + }) + .then((response) => response.json()) + .then((response) => { + dispatch(setAccountAccessToken(response.access_token)); + dispatch(loadAccountList(response.access_token, id)); + }); +}; diff --git a/src/Personal/openBanking/index.jsx b/src/Personal/openBanking/index.jsx new file mode 100644 index 0000000..c2d6f6a --- /dev/null +++ b/src/Personal/openBanking/index.jsx @@ -0,0 +1,146 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { View, ScrollView, Dimensions } from 'react-native'; +import { Button, Image, ListItem } from 'react-native-elements'; +import ModalDropdown from 'react-native-modal-dropdown'; +import { LinearGradient } from 'expo-linear-gradient'; +import PropTypes from 'prop-types'; +import { NavigationScreenPropType } from 'react-navigation'; +import GradientIcon from '../../Common/GradientIcon'; +import utils from '../../Stats/utils'; +import styles from '../../Common/themeStyleLight'; +import { + loadBankList, setBankIDCode, getAccessToken, +} from './actionCreator'; +import colors from '../../Common/Color'; + +const opBank = require('../../../assets/openBankingHead.png'); + +const OpenBanking = ({ navigation }) => { + const dispatch = useDispatch(); + const openBanking = useSelector((state) => state.openBanking); + const [targetBank, setTargetBank] = useState(''); + let interval; + + useEffect(() => { + if (openBanking.bankList.length === 0) { + dispatch(loadBankList()); + } + + if (openBanking.accountCode && !openBanking.accessToken) { + dispatch(getAccessToken(openBanking.accountCode, openBanking.accountID)); + } + }); + + const goAuth = (val) => { + const { id } = openBanking.bankList + .filter((item) => item.attributes.full_name === val)[0]; + + fetch(`https://oauth-sandbox.fintecture.com/ais/v1/provider/${id}/authorize?response_type=code&redirect_uri=http://122.51.72.108:8080`, { + headers: { + app_id: 'e9ac45bb-dd82-45a0-a092-c5a1bf398581', + Accept: 'application/json', + }, + }).then((response) => response.json()) + .then((response) => { + navigation.navigate({ routeName: 'WebView', params: { url: response.url } }); + }); + + interval = setInterval(() => { + if (!openBanking.accountID) { + fetch('http://122.51.72.108:8080', { method: 'post' }) + .then((response) => response.json()) + .then((response) => { + clearInterval(interval); + dispatch(setBankIDCode(response.id, response.code)); + navigation.navigate('OpenBanking'); + }) + .catch(() => {}); + } + }, 2000); + }; + + if (openBanking.accountList && openBanking.accountList.length > 0) { + return ( + <View style={{ alignItems: 'center' }}> + <View style={{ alignItems: 'center', paddingTop: 100 }}> + <Image source={opBank} style={{ width: 350, height: 350 }} /> + </View> + <ScrollView style={{ flexDirection: 'column', height: 250, width: Dimensions.get('window').width * 0.9 }}> + {openBanking.transactions.map((item, idx) => ( + <ListItem + key={`InfoGroup:${idx + 1}`} + leftElement={( + <GradientIcon + name="creditcard" + color="#9e87fc" + iconFamily="antdesign" + /> + )} + title={item.attributes.communication} + titleStyle={styles.listHeading} + subtitle={item.attributes.value_date} + subtitleStyle={{ color: 'grey' }} + rightTitle={utils.transType(item.attributes.amount, 'Expense')} + rightTitleStyle={styles.listHeading} + containerStyle={{ backgroundColor: 'rgba(255,0, 0, 0)' }} + /> + ))} + </ScrollView> + <LinearGradient + colors={colors.gradientColor.green} + start={[0.1, 0.9]} + end={[0.9, 0.1]} + style={{ + borderRadius: 24, paddingTop: 3, paddingBottom: 3, width: 240, marginTop: 50, + }} + > + <Button title="Add" type="clear" titleStyle={{ color: 'white' }} onPress={() => {}} /> + </LinearGradient> + </View> + + ); + } + return ( + <View> + <View style={{ alignItems: 'center', paddingTop: 100 }}> + <Image source={opBank} style={{ width: 350, height: 350 }} /> + </View> + <View style={{ alignItems: 'center', paddingTop: 100 }}> + <ModalDropdown + style={{ + borderWidth: 1, borderColor: '#410093', padding: 10, borderRadius: 5, width: 205, + }} + enableEmptySections + textStyle={{ fontSize: 15 }} + dropdownTextStyle={{ fontSize: 13 }} + dropdownStyle={{ width: 190 }} + defaultValue="Select your bank..." + options={openBanking.bankList.map((item) => item.attributes.full_name)} + onSelect={(idx, val) => setTargetBank(val)} + /> + + <LinearGradient + colors={colors.gradientColor.green} + start={[0.1, 0.9]} + end={[0.9, 0.1]} + style={{ + borderRadius: 24, paddingTop: 3, paddingBottom: 3, width: 240, marginTop: 50, + }} + > + <Button title="Authorize" type="clear" titleStyle={{ color: 'white' }} onPress={() => { goAuth(targetBank); }} /> + </LinearGradient> + </View> + </View> + ); +}; + +OpenBanking.propTypes = { + navigation: PropTypes.shape(NavigationScreenPropType), +}; + +OpenBanking.defaultProps = { + navigation: {}, +}; + +export default OpenBanking; diff --git a/src/Personal/openBanking/obReducer.js b/src/Personal/openBanking/obReducer.js new file mode 100644 index 0000000..c0582dd --- /dev/null +++ b/src/Personal/openBanking/obReducer.js @@ -0,0 +1,45 @@ +import { actionType } from './actionCreator'; + +const initialOB = { + bankList: [], + accountID: null, + accountCode: null, + accessToken: null, + accountList: [], + transactions: [], +}; + +const backupState = (state = initialOB, action) => { + switch (action.type) { + case actionType.SET_BANK_LIST: + return { + ...state, + bankList: action.payload, + }; + case actionType.SET_BANK_ID_CODE: + return { + ...state, + accountID: action.payload.id, + accountCode: action.payload.code, + }; + case actionType.SET_BANK_ACCESS_TOKEN: + return { + ...state, + accessToken: action.payload, + }; + case actionType.SET_ACCOUNT_LIST: + return { + ...state, + accountList: action.payload, + }; + case actionType.SET_ACCOUNT_TRANSACTION: + return { + ...state, + transactions: action.payload, + }; + default: + return state; + } +}; + +export default backupState; diff --git a/src/Personal/personalStackNavigation.js b/src/Personal/personalStackNavigation.js new file mode 100644 index 0000000..57a3daa --- /dev/null +++ b/src/Personal/personalStackNavigation.js @@ -0,0 +1,51 @@ +import { createAppContainer } from 'react-navigation'; +import { createStackNavigator } from 'react-navigation-stack'; +import Index from './index'; +import Register from './Register'; +import Login from './Login'; +import FamilyPage from './familyGroup/index'; +import OpenBanking from './openBanking/index'; +import WebView from './openBanking/WebView'; + +const MainNavigator = createStackNavigator({ + Index: { + screen: Index, + navigationOptions: { + header: null, + }, + }, + Register: { + screen: Register, + navigationOptions: { + header: null, + }, + }, + Login: { + screen: Login, + navigationOptions: { + header: null, + }, + }, + OpenBanking: { + screen: OpenBanking, + navigationOptions: { + header: null, + }, + }, + WebView: { + screen: WebView, + navigationOptions: { + header: null, + }, + }, + FamilyPage: { + screen: FamilyPage, + navigationOptions: { + header: null, + }, + }, +}); + +const personalNavigator = createAppContainer(MainNavigator); + +export default personalNavigator; diff --git a/src/Personal/registerViewStyle.js b/src/Personal/registerViewStyle.js new file mode 100644 index 0000000..a676244 --- /dev/null +++ b/src/Personal/registerViewStyle.js @@ -0,0 +1,88 @@ +import { StyleSheet } from 'react-native'; + +const btnShadow = { + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.70, + shadowRadius: 4.65, + elevation: 8, +}; + +const RegisterView = StyleSheet.create({ + registerForm: { + alignContent: 'center', + paddingLeft: 10, + paddingRight: 10, + flex: 1, + marginTop: 50, + marginBottom: 50, + flexDirection: 'column', + justifyContent: 'center', + }, + icon: { + alignItems: 'center', + paddingBottom: 50, + }, + inputIcon: { + marginLeft: 0, + marginRight: 5, + }, + inputSection: { marginBottom: 20 }, + heading: { + color: 'gray', + }, + button: { + alignSelf: 'stretch', + alignItems: 'center', + padding: 20, + backgroundColor: '#59cbbd', + marginTop: 30, + }, + buttonText: { + color: '#fff', + fontWeight: 'bold', + }, + note: { + color: 'red', + width: 'auto', + marginLeft: 10, + }, + + container: { + flex: 1, + backgroundColor: '#fff', + paddingLeft: 20, + paddingRight: 20, + paddingTop: 50, + flexDirection: 'row', + justifyContent: 'space-around', + alignContent: 'flex-end', + }, + btnShadow: { + shadowColor: '#70ea93', + ...btnShadow, + }, + + disabledBtnShadow: { + shadowColor: '#ababab', + ...btnShadow, + }, + errorBox: { + backgroundColor: '#f3dee0', + paddingTop: 10, + paddingBottom: 10, + marginLeft: 10, + marginRight: 10, + borderRadius: 5, + }, + + errorText: { + color: '#eb3d3d', + textAlign: 'center', + }, + +}); + +export default RegisterView; diff --git a/src/Personal/restoreReducer.js b/src/Personal/restoreReducer.js new file mode 100644 index 0000000..0be3f97 --- /dev/null +++ b/src/Personal/restoreReducer.js @@ -0,0 +1,36 @@ +import { actionType } from './actionCreator'; + +const initialRestore = { + isInProgress: false, + error: '', + lastRestoreDate: null, +}; + +const restoreState = (state = initialRestore, action) => { + switch (action.type) { + case actionType.RESTORE_START: + return { + ...state, + isInProgress: true, + error: '', + }; + case actionType.RESTORE_SUCCESSFUL: + return { + ...state, + isInProgress: false, + lastRestoreDate: action.payload, + error: '', + }; + case actionType.RESTORE_FAILED: + return { + ...state, + isInProgress: false, + error: action.payload, + lastRestoreDate: null, + }; + default: + return state; + } +}; + +export default restoreState; diff --git a/src/Personal/themeSelection/SelectBtn.jsx b/src/Personal/themeSelection/SelectBtn.jsx new file mode 100644 index 0000000..6e90428 --- /dev/null +++ b/src/Personal/themeSelection/SelectBtn.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Button } from 'react-native-elements'; +import PropTypes from 'prop-types'; +import styles from '../../Common/themeStyleLight'; + +export default function SelectBtn({ title, onClick, clickedMode }) { + return ( + <Button + title={title} + type="outline" + buttonStyle={styles.themeBtn} + disabledStyle={styles.disabledThemeBtn} + disabledTitleStyle={{ color: 'white' }} + titleStyle={{ fontSize: 16, color: '#5C6BC0' }} + disabled={title === clickedMode} + onPress={onClick} + /> + ); +} + +SelectBtn.propTypes = { + onClick: PropTypes.func.isRequired, + title: PropTypes.string.isRequired, + clickedMode: PropTypes.string.isRequired, +}; diff --git a/src/Personal/themeSelection/ThemeSelection.jsx b/src/Personal/themeSelection/ThemeSelection.jsx new file mode 100644 index 0000000..9d2ccf5 --- /dev/null +++ b/src/Personal/themeSelection/ThemeSelection.jsx @@ -0,0 +1,23 @@ +import React, { useState } from 'react'; +import { View } from 'react-native'; +import { useDispatch, useSelector } from 'react-redux'; +import { updateMode } from '../../Common/theme/actionCreator'; +import SelectBtn from './SelectBtn'; + +export default function ThemeSelection() { + const theme = useSelector((state) => state.theme); + const dispatch = useDispatch(); + const [mode, setMode] = useState(theme.themeMode); + + const updateTheme = (selectedMode) => { + dispatch(updateMode(selectedMode)); + setMode(selectedMode); + }; + + return ( + <View style={{ flexDirection: 'row' }}> + <SelectBtn title="Light" clickedMode={mode} onClick={() => updateTheme('Light')} /> + <SelectBtn title="Dark" clickedMode={mode} onClick={() => updateTheme('Dark')} /> + </View> + ); +} diff --git a/src/Personal/userReducer.js b/src/Personal/userReducer.js new file mode 100644 index 0000000..b04ca7c --- /dev/null +++ b/src/Personal/userReducer.js @@ -0,0 +1,70 @@ +import { actionType } from './actionCreator'; + +const initialUser = { + username: '', + email: '', + accessToken: '', + status: '', + message: '', +}; + +const user = (state = initialUser, action) => { + switch (action.type) { + case actionType.SIGNUP_START: + return { + ...state, + username: '', + email: '', + accessToken: '', + status: '', + message: '', + }; + case actionType.SIGNUP_SUCCESSFUL: + return { + ...state, + username: action.payload.username, + email: action.payload.email, + accessToken: action.payload.accessToken, + status: '', + }; + case actionType.SIGNUP_FAILED: + return { + ...state, + accessToken: '', + status: action.payload.status, + message: action.payload.message, + }; + case actionType.LOGOUT_SUCCESSFUL: + return { + ...state, + accessToken: '', + }; + case actionType.LOGIN_START: + return { + ...state, + email: '', + accessToken: '', + status: '', + message: '', + }; + case actionType.LOGIN_FAILED: + return { + ...state, + accessToken: '', + status: action.payload.status, + message: action.payload.message, + }; + case actionType.LOGIN_SUCCESSFUL: + return { + ...state, + email: action.payload.email, + accessToken: action.payload.accessToken, + status: '', + username: action.payload.username, + }; + default: + return state; + } +}; + +export default user; diff --git a/src/Personal/utils.js b/src/Personal/utils.js new file mode 100644 index 0000000..5b9165e --- /dev/null +++ b/src/Personal/utils.js @@ -0,0 +1,26 @@ +function emailValidation(email) { + const validateEmail = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return validateEmail.test(String(email).toLowerCase()); +} + +function passwordValidation(password) { + const validatePassword = /^[a-zA-Z0-9!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]{8,}$/; + return validatePassword.test(password); +} + +function validateSignup(username, password, confirmPassword, email) { + return username !== '' + && password === confirmPassword + && emailValidation(email) + && passwordValidation(password); +} +function loginValidation(email, password) { + return emailValidation(email) && passwordValidation(password); +} + +export default { + emailValidation, + passwordValidation, + validateSignup, + loginValidation, +}; diff --git a/src/Stats/EmptyHistory.jsx b/src/Stats/EmptyHistory.jsx index 054c382..c2abd85 100644 --- a/src/Stats/EmptyHistory.jsx +++ b/src/Stats/EmptyHistory.jsx @@ -1,6 +1,21 @@ import React from 'react'; -import { View, Text } from 'react-native'; +import { View } from 'react-native'; +import { Image } from 'react-native-elements'; +import { useSelector } from 'react-redux'; +import setThemeStyle from '../Common/theme/setThemeStyle'; + +const img = require('../../assets/empty_box.png'); export default function EmptyHistory() { - return (<View><Text>You haven"t added any transactions yet</Text></View>); + const { themeMode } = useSelector((state) => state.theme); + const styles = setThemeStyle(themeMode); + + return ( + <View style={[styles.card, { alignItems: 'center', justifyContent: 'center', paddingVertical: 30 }]}> + <Image + source={img} + style={{ width: 220, height: 220 }} + /> + </View> + ); } diff --git a/src/Stats/TransList.jsx b/src/Stats/TransList.jsx index af6531c..b3afbc1 100644 --- a/src/Stats/TransList.jsx +++ b/src/Stats/TransList.jsx @@ -1,41 +1,64 @@ -import React from 'react'; -import { View, Text } from 'react-native'; +import React, { useState, useEffect } from 'react'; +import { + View, Text, Animated, +} from 'react-native'; import { ListItem } from 'react-native-elements'; +import { useSelector } from 'react-redux'; import GradientIcon from '../Common/GradientIcon'; -import styles from '../Common/themeStyle'; import utils from './utils'; +import setThemeStyle from '../Common/theme/setThemeStyle'; export default function TransList(props) { const { transactions } = props; + const { themeMode } = useSelector((state) => state.theme); + const styles = setThemeStyle(themeMode); + const [fadeAnim] = useState(new Animated.Value(0)); + + useEffect(() => { + Animated.timing(fadeAnim, { + toValue: 1, + duration: 600, + }).start(); + + return () => Animated.timing(fadeAnim, { + toValue: 0, + duration: 0, + }).start(); + }, [transactions]); return ( transactions.map((groupedRecordsByDate, index) => ( - <View style={styles.card} key={`DateGroup:${index + 1}`}> - <View style={styles.cardAlign}> - <Text style={styles.cardHeader}> - {groupedRecordsByDate[0].date} - </Text> - <Text style={styles.cardHeader}> - {utils.sumAmount(groupedRecordsByDate)} - </Text> - </View> - {groupedRecordsByDate.map((item, idx) => ( - <ListItem - key={`InfoGroup:${idx + 1}`} - leftElement={( - <GradientIcon - name={item.label ? item.label.icon : 'home'} - color={item.label && item.label.color ? item.label.color : '#9e87fc'} - iconFamily={item.label && item.label.iconFamily ? item.label.iconFamily : ''} - /> + <Animated.View style={{ opacity: fadeAnim }} key={`DateGroup:${index + 1}`}> + <View style={styles.card}> + <View style={styles.cardAlign}> + <Text style={styles.cardHeader}> + {groupedRecordsByDate[0].date} + </Text> + <Text style={styles.cardHeader}> + {utils.sumAmount(groupedRecordsByDate)} + </Text> + </View> + {groupedRecordsByDate.map((item, idx) => ( + <ListItem + key={`InfoGroup:${idx + 1}`} + leftElement={( + <GradientIcon + name={item.labelName ? item.labelName.icon : 'home'} + color={item.labelName && item.labelName.color ? item.labelName.color : '#9e87fc'} + iconFamily={item.labelName && item.labelName.iconFamily ? item.labelName.iconFamily : ''} + /> )} - title={item.label ? item.label.name : 'Unknown'} - subtitle={item.comment ? item.comment : 'Unknown'} - subtitleStyle={{ color: 'grey' }} - rightTitle={utils.transType(item.amount, item.type)} - /> - ))} - </View> + title={item.labelName ? item.labelName.name : 'Unknown'} + titleStyle={styles.listHeading} + subtitle={item.creator ? item.creator : 'Unknown'} + subtitleStyle={item.creator ? { color: 'grey' } : { display: 'none' }} + rightTitle={utils.transType(item.amount, item.type)} + rightTitleStyle={styles.listHeading} + containerStyle={{ backgroundColor: 'rgba(255,0, 0, 0)' }} + /> + ))} + </View> + </Animated.View> )) ); } diff --git a/src/Stats/actionCreator.js b/src/Stats/actionCreator.js index 57004b6..cbfdc5d 100644 --- a/src/Stats/actionCreator.js +++ b/src/Stats/actionCreator.js @@ -1,12 +1,23 @@ export const actionType = { ADD_TRANS: 'ADD_TRANS', + RESTORE_TRANS: 'RESTORE_TRANS', }; -export function addNewTransaction(type, date, amount, label) { +export function addNewTransaction(type, date, amount, labelName) { return { type: actionType.ADD_TRANS, data: { - type, date, amount, label, + type, + date, + amount, + labelName, }, }; } + +export function restoreTransactions(dataSet) { + return { + type: actionType.RESTORE_TRANS, + dataSet, + }; +} diff --git a/src/Stats/actionCreator.test.js b/src/Stats/actionCreator.test.js new file mode 100644 index 0000000..f6a2152 --- /dev/null +++ b/src/Stats/actionCreator.test.js @@ -0,0 +1,43 @@ +import { addNewTransaction, restoreTransactions } from './actionCreator'; + +test('test action', () => { + const result = { + type: 'ADD_TRANS', + data: { + type: 'Expense', + date: 'December 24th 2019', + amount: '11.00', + labelName: { + color: '#aa78db', + icon: 'shoppingcart', + iconFamily: 'antdesign', + name: 'Shopping', + }, + }, + }; + expect(addNewTransaction('Expense', 'December 24th 2019', '11.00', { + color: '#aa78db', + icon: 'shoppingcart', + iconFamily: 'antdesign', + name: 'Shopping', + })).toEqual(result); +}); + +test('test restoreTransactions', () => { + const dataSet = [{ + amount: '2255.00', + date: 1578537684, + labelName: { + color: '#1cd09d', + icon: 'attach-money', + iconFamily: '', + name: 'Bills & Fees', + }, + type: 'Expense', + }]; + const result = { + type: 'RESTORE_TRANS', + dataSet, + }; + expect(restoreTransactions(dataSet)).toEqual(result); +}); diff --git a/src/Stats/index.jsx b/src/Stats/index.jsx index 31d21ec..d5860f8 100644 --- a/src/Stats/index.jsx +++ b/src/Stats/index.jsx @@ -1,19 +1,21 @@ import React, { useState } from 'react'; import { View, ScrollView } from 'react-native'; -import { ListItem, Overlay, Button } from 'react-native-elements'; import { useSelector } from 'react-redux'; -import styles from '../Common/themeStyle'; import MainHeader from '../Common/MainHeader'; +import DateSlider from '../Common/DateSlider'; import utils from './utils'; -import DateSlider from './DateSlider'; import FilterBtn from './FilterBtn'; import TransList from './TransList'; import EmptyHistory from './EmptyHistory'; +import DateOverlay from '../Common/DateOverlay'; +import setThemeStyle from '../Common/theme/setThemeStyle'; const moment = require('moment'); -export default function Stats() { +export default function Trans() { const { transactions } = useSelector((state) => state.transactions); + const { themeMode } = useSelector((state) => state.theme); + const styles = setThemeStyle(themeMode); const [view, setCurrentView] = useState('month'); const [timePeriodOptions, setTimePeriod] = useState(utils.getDateSet(moment(), view)); const [isOverlayVisible, setOverlayVisibility] = useState(false); @@ -45,21 +47,21 @@ export default function Stats() { const processedTransactions = utils.groupTransactionsByDate(filteredTransactions); const totalExpense = calculateSumByType(filteredTransactions, 'Expense'); const totalIncome = calculateSumByType(filteredTransactions, 'Income'); + const navBarFunc = [ + { + name: 'filter', + func: () => setOverlayVisibility(true), + }, + ]; return ( <View style={{ flex: 1, flexDirection: 'column' }}> - <Overlay height={200} isVisible={isOverlayVisible}> - <View> - <View style={{ flexDirection: 'row', justifyContent: 'flex-end' }}> - <Button icon={{ name: 'close' }} type="clear" onPress={() => setOverlayVisibility(false)} /> - </View> - <View style={{ marginTop: 20 }}> - <ListItem title="Month" topDivider bottomDivider onPress={() => updateHeaderView('month')} /> - <ListItem title="Year" bottomDivider onPress={() => updateHeaderView('year')} /> - </View> - </View> - </Overlay> - <MainHeader title="Activity" onPressBtn={() => setOverlayVisibility(true)} btnName="filter" /> + <DateOverlay + isOverlayVisible={isOverlayVisible} + onPressBtn={(viewValue) => updateHeaderView(viewValue)} + onPressClose={() => setOverlayVisibility(false)} + /> + <MainHeader title="Transactions" btnType={navBarFunc} /> <DateSlider viewSet={timePeriodOptions} onPressBtn={(value, type) => setTimePeriod(utils.getDateSet(value, type))} diff --git a/src/Stats/reducer.js b/src/Stats/reducer.js index 0beee6b..4d9eb7b 100644 --- a/src/Stats/reducer.js +++ b/src/Stats/reducer.js @@ -11,6 +11,11 @@ export default (state = initialState, action) => { ...state, transactions: [...state.transactions, { ...action.data }], }; + case actionType.RESTORE_TRANS: + return { + ...state, + transactions: action.dataSet, + }; default: return state; } diff --git a/src/Stats/utils.js b/src/Stats/utils.js index ed43b51..0572113 100644 --- a/src/Stats/utils.js +++ b/src/Stats/utils.js @@ -55,6 +55,23 @@ const filterTransactionsByDate = (dataList, range, view) => { } }; +const filterData = (dataList, range, view) => { + const monthRange = range.format('MMM YYYY'); + const yearRange = range.format('YYYY'); + let result; + switch (view) { + case 'month': + result = dataList.filter((value) => moment(value[0].date, 'MMMM Do YYYY').format('MMM YYYY') === monthRange); + break; + case 'year': + result = dataList.filter((value) => moment(value[0].date, 'MMMM Do YYYY').format('YYYY') === yearRange); + break; + default: + break; + } + return result; +}; + const filterTransactionByType = (dataList, type) => (type === 'all' ? dataList : dataList.filter((item) => (item.type === type))); const transType = (amount, type) => (type === 'Expense' ? `-$${amount}` : `+$${amount}`); @@ -64,6 +81,7 @@ export default { sumAmount, groupTransactionsByDate, getDateSet, + filterData, filterTransactionsByDate, filterTransactionByType, transType, diff --git a/src/TransCreator/CalculatorKeyboard.jsx b/src/TransCreator/CalculatorKeyboard.jsx index dcf6072..826d963 100644 --- a/src/TransCreator/CalculatorKeyboard.jsx +++ b/src/TransCreator/CalculatorKeyboard.jsx @@ -1,6 +1,8 @@ import React from 'react'; import { View } from 'react-native'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; +import setThemeStyle from '../Common/theme/setThemeStyle'; import { append, removeLast, keyboardLayout, getResult, } from './Calculator'; @@ -15,7 +17,12 @@ export default function CalculatorKeyboard({ transDate, setTransDate, onExpressionChange, + onPressCheckBox, + checkState, }) { + const { themeMode } = useSelector((state) => state.theme); + const theme = setThemeStyle(themeMode); + const wrapResult = (exp) => ({ expression: exp, result: getResult(exp), @@ -43,10 +50,10 @@ export default function CalculatorKeyboard({ return ( <View style={styles.keyboardLayout}> - <DateSelector transDate={transDate} setTransDate={setTransDate} /> - <View style={styles.keyboardRowLayout}> + <DateSelector transDate={transDate} setTransDate={setTransDate} onPressCheckBox={() => onPressCheckBox(!checkState)} checkState={checkState} /> + <View style={[styles.keyboardRowLayout, { backgroundColor: theme.mainColor.backgroundColor }]}> {keyboardLayout.map((row, idx) => ( - <View key={`row${idx + 1}`}> + <View key={`row${idx + 1}`} > {row.map((cell, idx2) => ( <View key={`cell${idx + 1}-${idx2 + 1}`}> <KeyboardButton btnVal={cell.toString()} pressHandler={pressHandler} /> diff --git a/src/TransCreator/DateSelector.jsx b/src/TransCreator/DateSelector.jsx index 3bfaace..5866d18 100644 --- a/src/TransCreator/DateSelector.jsx +++ b/src/TransCreator/DateSelector.jsx @@ -1,34 +1,41 @@ import React, { useState } from 'react'; import { MaterialIcons } from '@expo/vector-icons'; import { View, Text } from 'react-native'; +import { CheckBox } from 'react-native-elements'; import DateTimePicker from 'react-native-modal-datetime-picker'; import moment from 'moment'; import PropTypes from 'prop-types'; import styles from './Style'; -const DateSelector = ({ transDate, setTransDate }) => { +const DateSelector = ({ transDate, setTransDate, onPressCheckBox, checkState }) => { const [isDatePickerVisible, setDatePickerVisibility] = useState(false); + return ( - <View style={styles.dateSection}> - <MaterialIcons name="date-range" style={styles.dateItemIcon} /> - <View style={styles.dateView}> - <Text - onPress={() => setDatePickerVisibility(true)} - style={{ color: 'grey' }} - > - {moment.unix(transDate).format('MM/DD/YYYY')} - </Text> - <DateTimePicker - isVisible={isDatePickerVisible} - mode="date" - date={new Date(moment.unix(transDate).format('YYYY-MM-DD'))} - onCancel={() => setDatePickerVisibility(false)} - onConfirm={(date) => { - setTransDate(moment(date).unix()); - setDatePickerVisibility(false); - }} - titleStyle={{ color: '#5C6BC0' }} - /> + <View style={[styles.dateSection, { justifyContent: 'space-between', height: 50 }]}> + <View style={{ flexDirection: 'row' }}> + <MaterialIcons name="date-range" style={styles.dateItemIcon} /> + <View style={styles.dateView}> + <Text + onPress={() => setDatePickerVisibility(true)} + style={{ color: 'grey' }} + > + {moment.unix(transDate).format('MM/DD/YYYY')} + </Text> + <DateTimePicker + isVisible={isDatePickerVisible} + mode="date" + date={new Date(moment.unix(transDate).format('YYYY-MM-DD'))} + onCancel={() => setDatePickerVisibility(false)} + onConfirm={(date) => { + setTransDate(moment(date).unix()); + setDatePickerVisibility(false); + }} + titleStyle={{ color: '#5C6BC0' }} + /> + </View> + </View> + <View> + <CheckBox title="Family" onPress={onPressCheckBox} checkedColor="#5C6BC0" checked={checkState} containerStyle={{ paddingHorizontal: 4, paddingVertical: 2, borderWidth: 0 }} /> </View> </View> ); @@ -37,11 +44,14 @@ const DateSelector = ({ transDate, setTransDate }) => { DateSelector.propTypes = { transDate: PropTypes.number, setTransDate: PropTypes.func, + onPressCheckBox: PropTypes.func.isRequired, + checkState: PropTypes.bool, }; DateSelector.defaultProps = { transDate: moment().unix(), setTransDate: null, + checkState: false, }; export default DateSelector; diff --git a/src/TransCreator/IconButton.jsx b/src/TransCreator/IconButton.jsx index 4de6a62..775bbe4 100644 --- a/src/TransCreator/IconButton.jsx +++ b/src/TransCreator/IconButton.jsx @@ -1,21 +1,27 @@ import React from 'react'; import { Button } from 'react-native-elements'; +import { useSelector } from 'react-redux'; + import PropTypes from 'prop-types'; -import btnColor from '../Common/Color'; +import setThemeStyle from '../Common/theme/setThemeStyle'; import styles from './Style'; -const IconButton = ({ iconName, onPress }) => ( - <Button - type="outline" - icon={{ - name: iconName, - size: 25, - color: btnColor.grey, - }} - buttonStyle={styles.keyboardBtn} - onPress={onPress} - /> -); +const IconButton = ({ iconName, onPress }) => { + const { themeMode } = useSelector((state) => state.theme); + const theme = setThemeStyle(themeMode); + return ( + <Button + type="outline" + icon={{ + name: iconName, + size: 25, + color: theme.mainColor.color, + }} + buttonStyle={[styles.keyboardBtn, { borderColor: theme.mainColor.borderColor }]} + onPress={onPress} + /> + ); +}; IconButton.propTypes = { iconName: PropTypes.string.isRequired, diff --git a/src/TransCreator/KeyboardButton.jsx b/src/TransCreator/KeyboardButton.jsx index 1d36aa7..1e65066 100644 --- a/src/TransCreator/KeyboardButton.jsx +++ b/src/TransCreator/KeyboardButton.jsx @@ -1,19 +1,25 @@ import React from 'react'; import { Button } from 'react-native-elements'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import btnColor from '../Common/Color'; import IconButton from './IconButton'; import styles from './Style'; +import setThemeStyle from '../Common/theme/setThemeStyle'; -const NumberBtn = ({ btnVal, onPress }) => ( - <Button - type="outline" - buttonStyle={styles.keyboardBtn} - title={btnVal.toString()} - titleStyle={{ color: btnColor.grey }} - onPress={onPress} - /> -); +const NumberBtn = ({ btnVal, onPress }) => { + const { themeMode } = useSelector((state) => state.theme); + const theme = setThemeStyle(themeMode); + return ( + <Button + type="outline" + buttonStyle={[styles.keyboardBtn, { borderColor: theme.mainColor.borderColor }]} + title={btnVal.toString()} + titleStyle={{ color: theme.mainColor.color }} + onPress={onPress} + /> + ); +}; const KeyboardButton = ({ btnVal, pressHandler }) => { switch (btnVal) { diff --git a/src/TransCreator/LabelGroup.jsx b/src/TransCreator/LabelGroup.jsx index 25d67e7..47d486e 100644 --- a/src/TransCreator/LabelGroup.jsx +++ b/src/TransCreator/LabelGroup.jsx @@ -1,62 +1,67 @@ import React from 'react'; import { View, TouchableHighlight } from 'react-native'; import { Text, Icon } from 'react-native-elements'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { LinearGradient } from 'expo-linear-gradient'; import styles from './Style'; import Labels from '../Common/Labels'; -import theme from '../Common/themeStyle'; +import setThemeStyle from '../Common/theme/setThemeStyle'; import themeColor from '../Common/Color'; import TransactionTypeSelector from './TransactionTypeSelector'; const LabelGroup = ({ transType, setTransType, transLabel, setTransLabel, }) => { + const { themeMode } = useSelector((state) => state.theme); + const theme = setThemeStyle(themeMode); const labels = (transType === 'Expense' ? Labels.Expense : Labels.Income); const getColorsForLabel = (item) => { if (transLabel.name === item.name) { return transType === 'Expense' ? themeColor.gradientColor.red : themeColor.gradientColor.green; } - return ['white', 'white']; + return [theme.mainColor.backgroundColor, theme.mainColor.backgroundColor]; }; return ( - <View style={theme.card}> + <> <TransactionTypeSelector transType={transType} setTransType={setTransType} /> - <View style={styles.labelContainer}> - {labels.map((item) => ( - <TouchableHighlight - key={item.name} - underlayColor="white" - onPress={() => setTransLabel(item)} - > - <LinearGradient - colors={getColorsForLabel(item)} - start={[0.1, 0.9]} - end={[0.9, 0.1]} - style={{ margin: 5, padding: 2, borderRadius: 5 }} + <View style={[theme.iconCard, { marginTop: 0 }]}> + <View style={styles.labelContainer}> + {labels.map((item) => ( + <TouchableHighlight + key={item.name} + underlayColor="white" + onPress={() => setTransLabel(item)} > - <View style={styles.labelItem}> - <Icon - name={item.icon} - type={item.iconFamily} - color={transLabel.name === item.name ? 'white' : item.color} - size={30} - /> - <Text style={transLabel.name === item.name - ? { color: 'white' } : { color: 'grey' }} - > - {item.name} - </Text> - </View> - </LinearGradient> - </TouchableHighlight> - ))} + <LinearGradient + colors={getColorsForLabel(item)} + start={[0.1, 0.9]} + end={[0.9, 0.1]} + style={{ margin: 5, padding: 2, borderRadius: 5 }} + > + <View style={styles.labelItem}> + <Icon + name={item.icon} + type={item.iconFamily} + color={transLabel.name === item.name ? 'white' : item.color} + size={30} + /> + <Text style={transLabel.name === item.name + ? { color: 'white' } : { color: 'grey' }} + > + {item.name} + </Text> + </View> + </LinearGradient> + </TouchableHighlight> + ))} + </View> </View> - </View> + </> ); }; diff --git a/src/TransCreator/PageBanner.jsx b/src/TransCreator/PageBanner.jsx index 2e54157..1255705 100644 --- a/src/TransCreator/PageBanner.jsx +++ b/src/TransCreator/PageBanner.jsx @@ -4,9 +4,9 @@ import { } from 'react-native'; import { Icon } from 'react-native-elements'; import { LinearGradient } from 'expo-linear-gradient'; - +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; -import themeStyle from '../Common/themeStyle'; +import setThemeStyle from '../Common/theme/setThemeStyle'; import themeColor from '../Common/Color'; import styles from './Style'; @@ -15,31 +15,32 @@ export default function PageBanner({ }) { let displayAmount = transAmount === '' ? '0.00' : transAmount; displayAmount = transType === 'Expense' ? `-$${displayAmount}` : `$${displayAmount}`; + const { themeMode } = useSelector((state) => state.theme); + const themeStyleLight = setThemeStyle(themeMode); return ( - transLabel.name - ? ( - <LinearGradient - colors={transType === 'Expense' ? themeColor.gradientColor.red : themeColor.gradientColor.green} - start={[0.1, 0.9]} - end={[0.9, 0.1]} - > - <View style={[themeStyle.headerFormat, styles.headerFormat]}> - <View style={styles.headerContainer}> - <Icon name={transLabel.icon} type={transLabel.iconFamily} color="#ffffff" size={40} /> - <Text style={[styles.headerText, { color: '#ffffff' }]}>{transLabel.name || 'undefined'}</Text> - </View> - <View style={styles.headerDigitSection}> - <Text style={styles.headerDigitResult}>{ displayAmount }</Text> - <Text style={styles.headerDigitExp}>{ expStr }</Text> - </View> + <LinearGradient + colors={transType === 'Expense' ? themeColor.gradientColor.red : themeColor.gradientColor.green} + start={[0.1, 0.9]} + end={[0.9, 0.1]} + > + {transLabel.name ? ( + <View style={[themeStyleLight.headerFormat, styles.headerFormat, { backgroundColor: 'rgba(255,0,0,0)' }]}> + <View style={styles.headerContainer}> + <Icon name={transLabel.icon} type={transLabel.iconFamily} color="#ffffff" size={40} /> + <Text style={[styles.headerText, { color: '#ffffff' }]}>{transLabel.name || 'undefined'}</Text> + </View> + <View style={styles.headerDigitSection}> + <Text style={styles.headerDigitResult}>{displayAmount}</Text> + <Text style={styles.headerDigitExp}>{expStr}</Text> </View> - </LinearGradient> - ) - : ( - <View style={[themeStyle.headerFormat, styles.headerFormat]}> - <Text style={styles.headerText}>Select a category</Text> </View> ) + : ( + <View style={[themeStyleLight.headerFormat, styles.headerFormat]}> + <Text style={[styles.headerText, { color: 'white' }]}>Select a category</Text> + </View> + )} + </LinearGradient> ); } diff --git a/src/TransCreator/Style.js b/src/TransCreator/Style.js index 1a29490..2c210d9 100644 --- a/src/TransCreator/Style.js +++ b/src/TransCreator/Style.js @@ -61,19 +61,28 @@ export default StyleSheet.create({ typeButtonGroup: { flexDirection: 'row', justifyContent: 'center', - marginBottom: 20, + marginTop: 15, }, + typeButton: { - borderRadius: 50, - width: 120, + borderTopRightRadius: 5, + borderTopLeftRadius: 5, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + borderBottomColor: '#F5F5F5', + width: 187, marginTop: 15, - paddingTop: 5, + paddingTop: 10, marginBottom: 0, - paddingBottom: 5, - borderColor: 'gray', - borderWidth: 1, - marginRight: 6, - marginLeft: 6, + paddingBottom: 10, + borderWidth: 0, + shadowColor: 'grey', + shadowOffset: { + width: 2, + height: 3, + }, + shadowOpacity: 0.08, + shadowRadius: 5, }, typeTitle: { color: 'grey', @@ -83,7 +92,8 @@ export default StyleSheet.create({ color: 'black', }, typeButtonActive: { - borderColor: 'black', + borderBottomColor: '#FF6F00', + borderBottomWidth: 2, }, headerFormat: { height: 140, diff --git a/src/TransCreator/TransactionTypeSelector.jsx b/src/TransCreator/TransactionTypeSelector.jsx index eec6c73..139754d 100644 --- a/src/TransCreator/TransactionTypeSelector.jsx +++ b/src/TransCreator/TransactionTypeSelector.jsx @@ -1,31 +1,41 @@ import React from 'react'; import { View } from 'react-native'; import { Button } from 'react-native-elements'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import styles from './Style'; +import setThemeStyle from '../Common/theme/setThemeStyle'; -const TransactionTypeSelector = ({ transType, setTransType }) => ( - <View style={styles.typeButtonGroup}> - <Button - buttonStyle={transType === 'Expense' - ? [styles.typeButton, styles.typeButtonActive] : styles.typeButton} - titleStyle={transType === 'Expense' - ? [styles.typeTitle, styles.typeTitleActive] : styles.typeTitle} - title="Expense" - type="outline" - onPress={() => setTransType('Expense')} - /> - <Button - buttonStyle={transType === 'Income' - ? [styles.typeButton, styles.typeButtonActive] : styles.typeButton} - titleStyle={transType === 'Income' - ? [styles.typeTitle, styles.typeTitleActive] : styles.typeTitle} - title="Income" - type="outline" - onPress={() => setTransType('Income')} - /> - </View> -); +const TransactionTypeSelector = ({ transType, setTransType }) => { + const { themeMode } = useSelector((state) => state.theme); + const theme = setThemeStyle(themeMode); + return ( + <View style={styles.typeButtonGroup}> + <Button + buttonStyle={transType === 'Expense' + ? [styles.typeButton, styles.typeButtonActive, { borderColor: theme.mainColor.color, backgroundColor: theme.mainColor.backgroundColor }] + : styles.typeButton} + titleStyle={transType === 'Expense' + ? [styles.typeTitle, styles.typeTitleActive, theme.mainColor] + : styles.typeTitle} + title="Expense" + type="outline" + onPress={() => setTransType('Expense')} + /> + <Button + buttonStyle={transType === 'Income' + ? [styles.typeButton, styles.typeButtonActive, { borderColor: theme.mainColor.color, backgroundColor: theme.mainColor.backgroundColor }] + : styles.typeButton} + titleStyle={transType === 'Income' + ? [styles.typeTitle, styles.typeTitleActive, theme.mainColor] + : styles.typeTitle} + title="Income" + type="outline" + onPress={() => setTransType('Income')} + /> + </View> + ); +}; TransactionTypeSelector.propTypes = { transType: PropTypes.string, diff --git a/src/TransCreator/index.jsx b/src/TransCreator/index.jsx index 07759c9..c94070c 100644 --- a/src/TransCreator/index.jsx +++ b/src/TransCreator/index.jsx @@ -1,28 +1,32 @@ import React, { useEffect, useState } from 'react'; import { Root } from 'native-base'; import { View, Alert } from 'react-native'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import moment from 'moment'; import { NavigationScreenPropType } from 'react-navigation'; import { addNewTransaction } from '../Stats/actionCreator'; +import { addFamilyTransactions } from '../FamilyTrans/actionCreator'; import LabelGroup from './LabelGroup'; import PageBanner from './PageBanner'; -import theme from '../Common/themeStyle'; import CalculatorKeyboard from './CalculatorKeyboard'; +import setThemeStyle from '../Common/theme/setThemeStyle'; const TransCreator = ({ navigation }) => { + const { themeMode } = useSelector((state) => state.theme); + const theme = setThemeStyle(themeMode); const [transAmount, setTransAmount] = useState(''); const [expStr, setExpStr] = useState(''); const [transType, setTransType] = useState('Expense'); const [transDate, setTransDate] = useState(moment().unix()); const [transLabel, setTransLabel] = useState({}); const [newTransInsertionSuccess, setNewTransInsertionSuccess] = useState(false); + const [isBoxChecked, setBoxChecked] = useState(false); const dispatch = useDispatch(); useEffect(() => { if (newTransInsertionSuccess) { - navigation.navigate('Stats'); + navigation.navigate('Trans'); setNewTransInsertionSuccess(false); } }); @@ -32,7 +36,17 @@ const TransCreator = ({ navigation }) => { Alert.alert('Please enter the amount'); return; } - + if (isBoxChecked) { + const data = { + transaction: { + amount: transAmount, + labelName: transLabel, + date: transDate, + type: transType, + }, + }; + dispatch(addFamilyTransactions(data)); + } dispatch(addNewTransaction(transType, transDate, transAmount, transLabel)); setNewTransInsertionSuccess(true); setTransAmount(''); @@ -76,6 +90,8 @@ const TransCreator = ({ navigation }) => { transDate={transDate} setTransDate={setTransDate} expStr={expStr} + onPressCheckBox={() => setBoxChecked(!isBoxChecked)} + checkState={isBoxChecked} /> </Root> ); diff --git a/src/reduxStore.js b/src/reduxStore.js index f74d591..6bc8a34 100644 --- a/src/reduxStore.js +++ b/src/reduxStore.js @@ -4,15 +4,33 @@ import { persistStore, persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'; import transactions from './Stats/reducer'; +import backupState from './Personal/backupReducer'; +import restoreState from './Personal/restoreReducer'; +import openBanking from './Personal/openBanking/obReducer'; +import user from './Personal/userReducer'; +import theme from './Common/theme/themeReducer'; +import searchedList from './Personal/familyGroup/familySearchReducer'; +import familyList from './Personal/familyGroup/familyReducer'; +import familyTrans from './FamilyTrans/familyTransReducer'; + const persistConfig = { key: 'root', storage, stateReconciler: autoMergeLevel2, + blacklist: ['openBanking', 'familyList'], }; const rootReducer = combineReducers({ transactions, + backupState, + user, + restoreState, + openBanking, + theme, + searchedList, + familyList, + familyTrans, }); const persistedReducer = persistReducer(persistConfig, rootReducer);