diff --git a/assets/images/avatars/room.png b/assets/images/avatars/room.png new file mode 100644 index 000000000000..dca457fbfdf7 Binary files /dev/null and b/assets/images/avatars/room.png differ diff --git a/assets/images/expensify_wordmark_white.svg b/assets/images/expensify_wordmark_white.svg new file mode 100644 index 000000000000..1ad7640b2602 --- /dev/null +++ b/assets/images/expensify_wordmark_white.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/assets/images/qrcode.svg b/assets/images/qrcode.svg new file mode 100644 index 000000000000..ead1d765b46a --- /dev/null +++ b/assets/images/qrcode.svg @@ -0,0 +1,3 @@ + + + diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 45b855a72730..7c6d1b9de3a9 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -21,6 +21,8 @@ const includeModules = [ 'react-native-flipper', 'react-native-google-places-autocomplete', '@react-navigation/drawer', + 'react-native-qrcode-svg', + 'react-native-view-shot', ].join('|'); const envToLogoSuffixMap = { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fc30bf617a24..f0d6720080e8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -541,6 +541,8 @@ PODS: - RCTTypeSafety - React-Core - ReactCommon/turbomodule/core + - react-native-view-shot (3.6.0): + - React-Core - react-native-webview (11.23.0): - React-Core - React-perflogger (0.71.2-alpha.3) @@ -698,7 +700,7 @@ PODS: - RNScreens (3.17.0): - React-Core - React-RCTImage - - RNSVG (13.5.0): + - RNSVG (13.9.0): - React-Core - SDWebImage (5.11.1): - SDWebImage/Core (= 5.11.1) @@ -781,6 +783,7 @@ DEPENDENCIES: - react-native-quick-sqlite (from `../node_modules/react-native-quick-sqlite`) - react-native-render-html (from `../node_modules/react-native-render-html`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-view-shot (from `../node_modules/react-native-view-shot`) - react-native-webview (from `../node_modules/react-native-webview`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) @@ -944,6 +947,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-render-html" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" + react-native-view-shot: + :path: "../node_modules/react-native-view-shot" react-native-webview: :path: "../node_modules/react-native-webview" React-perflogger: @@ -1089,6 +1094,7 @@ SPEC CHECKSUMS: react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a + react-native-view-shot: 705f999ac2a24e4e6c909c0ca65c732ed33ca2ff react-native-webview: e771bc375f789ebfa02a26939a57dbc6fa897336 React-perflogger: 9f7c5a9d22d104ae0348f38235ee636d156b5938 React-RCTActionSheet: 43e74f3a54967bb1d0e9ff261bc076843fb50ca1 @@ -1120,7 +1126,7 @@ SPEC CHECKSUMS: RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c RNReanimated: fbc356493970e3acddc15586b1bccb5eab3ff1ec RNScreens: 0df01424e9e0ed7827200d6ed1087ddd06c493f9 - RNSVG: 38ca962c970dbce1ca38991a5aebf26d163f9efb + RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 diff --git a/package-lock.json b/package-lock.json index 801117332f7c..141a7cb34f23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,12 +83,14 @@ "react-native-permissions": "^3.0.1", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#107b3786ae6bc155dec05c7fc5ee525d3421dc21", "react-native-plaid-link-sdk": "^10.0.0", + "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", "react-native-reanimated": "3.0.0-rc.10", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.4.1", "react-native-screens": "3.17.0", - "react-native-svg": "^13.5.0", + "react-native-svg": "^13.9.0", + "react-native-view-shot": "^3.6.0", "react-native-web-lottie": "^1.4.4", "react-native-webview": "^11.17.2", "react-pdf": "5.7.2", @@ -20909,6 +20911,11 @@ "version": "4.12.0", "license": "MIT" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "node_modules/dir-compare": { "version": "2.4.0", "dev": true, @@ -21594,6 +21601,11 @@ "node": ">= 4" } }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" + }, "node_modules/encodeurl": { "version": "1.0.2", "license": "MIT", @@ -33815,6 +33827,14 @@ "node": ">=8.0" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/pnp-webpack-plugin": { "version": "1.6.4", "dev": true, @@ -34274,6 +34294,173 @@ "version": "14.18.24", "license": "MIT" }, + "node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/qrcode/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/qrcode/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "dev": true, @@ -34945,6 +35132,20 @@ "react-native": ">=0.66.0" } }, + "node_modules/react-native-qrcode-svg": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.2.0.tgz", + "integrity": "sha512-rb2PgUwT8QpQyReVYNvzRY84AHsMh81354Tnkfp6MfqRbcdJURhnBWLBOO11pLMS6eXiwlq4SkcQxy88hRq+Dw==", + "dependencies": { + "prop-types": "^15.8.0", + "qrcode": "^1.5.1" + }, + "peerDependencies": { + "react": "*", + "react-native": ">=0.63.4", + "react-native-svg": "^13.2.0" + } + }, "node_modules/react-native-quick-sqlite": { "version": "8.0.0-beta.2", "resolved": "https://registry.npmjs.org/react-native-quick-sqlite/-/react-native-quick-sqlite-8.0.0-beta.2.tgz", @@ -35034,8 +35235,9 @@ } }, "node_modules/react-native-svg": { - "version": "13.5.0", - "license": "MIT", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.9.0.tgz", + "integrity": "sha512-Ey18POH0dA0ob/QiwCBVrxIiwflhYuw0P0hBlOHeY4J5cdbs8ngdKHeWC/Kt9+ryP6fNoEQ1PUgPYw2Bs/rp5Q==", "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3" @@ -35059,6 +35261,15 @@ "react-native-svg": ">=12.0.0" } }, + "node_modules/react-native-view-shot": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-3.6.0.tgz", + "integrity": "sha512-QUYGaIaAxQwOTydUzqGMooBwrg455cuOQgTloZ+gPO1QCUuLRdncCqrEMwKW5eUnN5U8JGMKeFRll2m6egOxtA==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-web": { "version": "0.18.12", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.12.tgz", @@ -55415,6 +55626,11 @@ } } }, + "dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "dir-compare": { "version": "2.4.0", "dev": true, @@ -55888,6 +56104,11 @@ "emojis-list": { "version": "3.0.0" }, + "encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" + }, "encodeurl": { "version": "1.0.2" }, @@ -63889,6 +64110,11 @@ } } }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, "pnp-webpack-plugin": { "version": "1.6.4", "dev": true, @@ -64202,6 +64428,130 @@ "version": "0.3.8", "dev": true }, + "qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "requires": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "qs": { "version": "6.11.0", "dev": true, @@ -64719,6 +65069,15 @@ "integrity": "sha512-WqU44tYzQoR/cuufD6GI7vOWTLcL9RXuEqfGaCynHdh2rmj3SC+mSEmXpg/LG0Q4E1XivkjfgF9tAOdlbnLMHQ==", "requires": {} }, + "react-native-qrcode-svg": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.2.0.tgz", + "integrity": "sha512-rb2PgUwT8QpQyReVYNvzRY84AHsMh81354Tnkfp6MfqRbcdJURhnBWLBOO11pLMS6eXiwlq4SkcQxy88hRq+Dw==", + "requires": { + "prop-types": "^15.8.0", + "qrcode": "^1.5.1" + } + }, "react-native-quick-sqlite": { "version": "8.0.0-beta.2", "resolved": "https://registry.npmjs.org/react-native-quick-sqlite/-/react-native-quick-sqlite-8.0.0-beta.2.tgz", @@ -64781,7 +65140,9 @@ } }, "react-native-svg": { - "version": "13.5.0", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.9.0.tgz", + "integrity": "sha512-Ey18POH0dA0ob/QiwCBVrxIiwflhYuw0P0hBlOHeY4J5cdbs8ngdKHeWC/Kt9+ryP6fNoEQ1PUgPYw2Bs/rp5Q==", "requires": { "css-select": "^5.1.0", "css-tree": "^1.1.3" @@ -64796,6 +65157,12 @@ "path-dirname": "^1.0.2" } }, + "react-native-view-shot": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-3.6.0.tgz", + "integrity": "sha512-QUYGaIaAxQwOTydUzqGMooBwrg455cuOQgTloZ+gPO1QCUuLRdncCqrEMwKW5eUnN5U8JGMKeFRll2m6egOxtA==", + "requires": {} + }, "react-native-web": { "version": "0.18.12", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.12.tgz", diff --git a/package.json b/package.json index bc8d960badd3..96038fbca440 100644 --- a/package.json +++ b/package.json @@ -118,12 +118,14 @@ "react-native-permissions": "^3.0.1", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#107b3786ae6bc155dec05c7fc5ee525d3421dc21", "react-native-plaid-link-sdk": "^10.0.0", + "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", "react-native-reanimated": "3.0.0-rc.10", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.4.1", "react-native-screens": "3.17.0", - "react-native-svg": "^13.5.0", + "react-native-svg": "^13.9.0", + "react-native-view-shot": "^3.6.0", "react-native-web-lottie": "^1.4.4", "react-native-webview": "^11.17.2", "react-pdf": "5.7.2", @@ -196,8 +198,8 @@ "metro-react-native-babel-preset": "^0.73.3", "mock-fs": "^4.13.0", "onchange": "^7.1.0", - "prettier": "^2.8.8", "portfinder": "^1.0.28", + "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", "react-native-clean-project": "^4.0.0-alpha4.0", "react-native-flipper": "https://gitpkg.now.sh/facebook/flipper/react-native/react-native-flipper?9cacc9b59402550eae866e0e81e5f0c2f8203e6b", diff --git a/src/CONST.js b/src/CONST.js index ad98362484b6..e1335306d47a 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -1097,6 +1097,7 @@ const CONST = { INFO: 'info', }, REPORT_DETAILS_MENU_ITEM: { + SHARE_CODE: 'shareCode', MEMBERS: 'member', SETTINGS: 'settings', LEAVE_ROOM: 'leaveRoom', diff --git a/src/ROUTES.js b/src/ROUTES.js index 1d5506f019e7..ec4730289e06 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -25,6 +25,7 @@ export default { HOME: '', SETTINGS: 'settings', SETTINGS_PROFILE: 'settings/profile', + SETTINGS_SHARE_CODE: 'settings/shareCode', SETTINGS_DISPLAY_NAME: 'settings/profile/display-name', SETTINGS_TIMEZONE: 'settings/profile/timezone', SETTINGS_TIMEZONE_SELECT: 'settings/profile/timezone/select', @@ -60,6 +61,8 @@ export default { REPORT, REPORT_WITH_ID: 'r/:reportID', getReportRoute: (reportID) => `r/${reportID}`, + REPORT_WITH_ID_DETAILS_SHARE_CODE: 'r/:reportID/details/shareCode', + getReportShareCodeRoute: (reportID) => `r/${reportID}/details/shareCode`, SELECT_YEAR: 'select-year', getYearSelectionRoute: (minYear, maxYear, currYear, backTo) => `select-year?min=${minYear}&max=${maxYear}&year=${currYear}&backTo=${backTo}`, diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js index d3c7c2f4bc57..70503706f112 100644 --- a/src/components/Icon/Expensicons.js +++ b/src/components/Icon/Expensicons.js @@ -77,6 +77,7 @@ import Pin from '../../../assets/images/pin.svg'; import Plus from '../../../assets/images/plus.svg'; import Printer from '../../../assets/images/printer.svg'; import Profile from '../../../assets/images/profile.svg'; +import QrCode from '../../../assets/images/qrcode.svg'; import QuestionMark from '../../../assets/images/question-mark-circle.svg'; import Receipt from '../../../assets/images/receipt.svg'; import ReceiptSearch from '../../../assets/images/receipt-search.svg'; @@ -197,6 +198,7 @@ export { Plus, Printer, Profile, + QrCode, QuestionMark, Receipt, ReceiptSearch, diff --git a/src/components/QRShare/QRShareWithDownload/index.js b/src/components/QRShare/QRShareWithDownload/index.js new file mode 100644 index 000000000000..8cb7be79f7ad --- /dev/null +++ b/src/components/QRShare/QRShareWithDownload/index.js @@ -0,0 +1,39 @@ +import React, {Component} from 'react'; +import fileDownload from '../../../libs/fileDownload'; +import QRShare from '..' +import {qrShareDefaultProps, qrSharePropTypes} from '../propTypes'; +import getQrCodeFileName from '../getQrCodeDownloadFileName'; + +class QRShareWithDownload extends Component { + qrShareRef = React.createRef(); + + constructor(props) { + super(props); + + this.download = this.download.bind(this); + } + + download() { + return new Promise((resolve, reject) => { + // eslint-disable-next-line es/no-optional-chaining + const svg = this.qrShareRef.current?.getSvg(); + if (svg == null) return reject(); + + svg.toDataURL((dataURL) => resolve(fileDownload(dataURL, getQrCodeFileName(this.props.title)))); + }); + } + + render() { + return ( + + ); + } +} +QRShareWithDownload.propTypes = qrSharePropTypes; +QRShareWithDownload.defaultProps = qrShareDefaultProps; + +export default QRShareWithDownload; diff --git a/src/components/QRShare/QRShareWithDownload/index.native.js b/src/components/QRShare/QRShareWithDownload/index.native.js new file mode 100644 index 000000000000..27f05038733a --- /dev/null +++ b/src/components/QRShare/QRShareWithDownload/index.native.js @@ -0,0 +1,36 @@ +import React, {Component} from 'react'; +import ViewShot from 'react-native-view-shot'; +import fileDownload from '../../../libs/fileDownload'; +import QRShare from '..' +import {qrShareDefaultProps, qrSharePropTypes} from '../propTypes'; +import getQrCodeFileName from '../getQrCodeDownloadFileName'; + + +class QRShareWithDownload extends Component { + qrCodeScreenshotRef = React.createRef(); + + constructor(props) { + super(props); + + this.download = this.download.bind(this); + } + + download() { + return this.qrCodeScreenshotRef.current.capture().then((uri) => fileDownload(uri, getQrCodeFileName(this.props.title))); + } + + render() { + return ( + + + + ); + } +} +QRShareWithDownload.propTypes = qrSharePropTypes; +QRShareWithDownload.defaultProps = qrShareDefaultProps; + +export default QRShareWithDownload; diff --git a/src/components/QRShare/getQrCodeDownloadFileName.js b/src/components/QRShare/getQrCodeDownloadFileName.js new file mode 100644 index 000000000000..cc3b38d42348 --- /dev/null +++ b/src/components/QRShare/getQrCodeDownloadFileName.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line rulesdir/display-name-property +const getQrCodeDownloadFileName = (title) => `${title}-ShareCode.png`; + +export default getQrCodeDownloadFileName; diff --git a/src/components/QRShare/index.js b/src/components/QRShare/index.js new file mode 100644 index 000000000000..014cddcf5090 --- /dev/null +++ b/src/components/QRShare/index.js @@ -0,0 +1,98 @@ +import React, {Component} from 'react'; +import QRCodeLibrary from 'react-native-qrcode-svg'; +import {View} from 'react-native'; +import withLocalize, {withLocalizePropTypes} from '../withLocalize'; +import defaultTheme from '../../styles/themes/default'; +import styles from '../../styles/styles'; +import Text from '../Text'; +import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions'; +import compose from '../../libs/compose'; +import variables from '../../styles/variables'; +import ExpensifyWordmark from '../../../assets/images/expensify-wordmark.svg'; +import {qrSharePropTypes, qrShareDefaultProps} from './propTypes' + +const propTypes = { + ...qrSharePropTypes, + ...windowDimensionsPropTypes, + ...withLocalizePropTypes, +}; + +class QRShare extends Component { + constructor(props) { + super(props); + + this.state = { + qrCodeSize: 0, + }; + + this.onLayout = this.onLayout.bind(this); + this.getSvg = this.getSvg.bind(this); + } + + onLayout(event) { + this.setState({ + qrCodeSize: event.nativeEvent.layout.width - variables.qrShareHorizontalPadding * 2, + }); + } + + getSvg() { + return this.svg; + } + + render() { + return ( + + + + + + (this.svg = svg)} + logoBackgroundColor="transparent" + logoSize={this.state.qrCodeSize * 0.3} + logoBorderRadius={this.state.qrCodeSize} + size={this.state.qrCodeSize} + backgroundColor={defaultTheme.highlightBG} + color={defaultTheme.text} + /> + + + {this.props.title} + + + {this.props.subtitle && ( + + {this.props.subtitle} + + )} + + ); + } +} +QRShare.propTypes = propTypes; +QRShare.defaultProps = qrShareDefaultProps; + +export default compose(withLocalize, withWindowDimensions)(QRShare); diff --git a/src/components/QRShare/propTypes.js b/src/components/QRShare/propTypes.js new file mode 100644 index 000000000000..bcb55f99ea69 --- /dev/null +++ b/src/components/QRShare/propTypes.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; + +const qrSharePropTypes = { + /** + * The QR code URL + */ + url: PropTypes.string.isRequired, + /** + * The title that is displayed below the QR Code (usually the user or report name) + */ + title: PropTypes.string.isRequired, + /** + * The subtitle which will be shown below the title (usually user email or workspace name) + * */ + subtitle: PropTypes.string, + /** + * The logo which will be display in the middle of the QR code + */ + logo: PropTypes.string, +}; + +const defaultProps = { + subtitle: undefined, + logo: undefined, +}; + +export {qrSharePropTypes, defaultProps as qrShareDefaultProps}; diff --git a/src/languages/en.js b/src/languages/en.js index 394342e6cada..c3543cd926e1 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -134,6 +134,8 @@ export default { zipCodeExampleFormat: ({zipSampleFormat}) => (zipSampleFormat ? `e.g. ${zipSampleFormat}` : ''), description: 'Description', with: 'with', + shareCode: 'Share code', + share: 'Share', }, attachmentPicker: { cameraPermissionRequired: 'Camera access', diff --git a/src/languages/es.js b/src/languages/es.js index de00f658fb16..b936272d4622 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -133,6 +133,8 @@ export default { zipCodeExampleFormat: ({zipSampleFormat}) => (zipSampleFormat ? `p. ej. ${zipSampleFormat}` : ''), description: 'Descripción', with: 'con', + shareCode: 'Compartir código', + share: 'Compartir', }, attachmentPicker: { cameraPermissionRequired: 'Permiso para acceder a la cámara', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index cc2b18ce47fc..7b33335fb86d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -160,6 +160,13 @@ const ReportDetailsModalStackNavigator = createModalStackNavigator([ }, name: 'Report_Details_Root', }, + { + getComponent: () => { + const ShareCodePage = require('../../../pages/home/report/ReportDetailsShareCodePage').default; + return ShareCodePage; + }, + name: 'Report_Details_Share_Code', + }, ]); const TaskModalStackNavigator = createModalStackNavigator([ @@ -296,6 +303,13 @@ const SettingsModalStackNavigator = createModalStackNavigator([ }, name: 'Settings_Root', }, + { + getComponent: () => { + const ShareCodePage = require('../../../pages/ShareCodePage').default; + return ShareCodePage; + }, + name: 'Settings_Share_Code', + }, { getComponent: () => { const SettingsWorkspacesPage = require('../../../pages/workspace/WorkspacesListPage').default; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 92b4414b99ae..1a7c7398caf0 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -30,6 +30,10 @@ export default { Settings_Root: { path: ROUTES.SETTINGS, }, + Settings_Share_Code: { + path: ROUTES.SETTINGS_SHARE_CODE, + exact: true, + }, Settings_Workspaces: { path: ROUTES.SETTINGS_WORKSPACES, exact: true, @@ -186,6 +190,7 @@ export default { Report_Details: { screens: { Report_Details_Root: ROUTES.REPORT_WITH_ID_DETAILS, + Report_Details_Share_Code: ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE, }, }, Report_Settings: { diff --git a/src/libs/fileDownload/index.android.js b/src/libs/fileDownload/index.android.js index 98bd5c975331..c14575881fea 100644 --- a/src/libs/fileDownload/index.android.js +++ b/src/libs/fileDownload/index.android.js @@ -43,26 +43,33 @@ function handleDownload(url, fileName) { const path = dirs.DownloadDir; const attachmentName = fileName || FileUtils.getAttachmentName(url); - // Fetching the attachment - const fetchedAttachment = RNFetchBlob.config({ - fileCache: true, - path: `${path}/${attachmentName}`, - addAndroidDownloads: { - useDownloadManager: true, - notification: false, - path: `${path}/Expensify/${attachmentName}`, - }, - }).fetch('GET', url); + const isLocalFile = url.startsWith('file://'); - let attachmentPath; + let attachmentPath = isLocalFile ? url : undefined; + let fetchedAttachment = Promise.resolve(); + + if (!isLocalFile) { + // Fetching the attachment + fetchedAttachment = RNFetchBlob.config({ + fileCache: true, + path: `${path}/${attachmentName}`, + addAndroidDownloads: { + useDownloadManager: true, + notification: false, + path: `${path}/Expensify/${attachmentName}`, + }, + }).fetch('GET', url); + } // Resolving the fetched attachment fetchedAttachment .then((attachment) => { - if (!attachment || !attachment.info()) { + if (!isLocalFile && (!attachment || !attachment.info())) { return Promise.reject(); } - attachmentPath = attachment.path(); + + if (!isLocalFile) attachmentPath = attachment.path(); + return RNFetchBlob.MediaCollection.copyToMediaStore( { name: attachmentName, diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index ee2c2fefb122..9ed7bfa86092 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -57,7 +57,14 @@ const defaultProps = { class ReportDetailsPage extends Component { getMenuItems() { - const menuItems = []; + const menuItems = [ + { + key: CONST.REPORT_DETAILS_MENU_ITEM.SHARE_CODE, + translationKey: 'common.shareCode', + icon: Expensicons.QrCode, + action: () => Navigation.navigate(ROUTES.getReportShareCodeRoute(this.props.report.reportID)), + }, + ]; if (ReportUtils.isArchivedRoom(this.props.report)) { return []; diff --git a/src/pages/ShareCodePage.js b/src/pages/ShareCodePage.js new file mode 100644 index 000000000000..8ce21ccabd65 --- /dev/null +++ b/src/pages/ShareCodePage.js @@ -0,0 +1,92 @@ +import React from 'react'; +import {View, ScrollView} from 'react-native'; +import ScreenWrapper from '../components/ScreenWrapper'; +import HeaderWithCloseButton from '../components/HeaderWithCloseButton'; +import Navigation from '../libs/Navigation/Navigation'; +import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; +import QRShareWithDownload from '../components/QRShare/QRShareWithDownload'; +import compose from '../libs/compose'; +import reportPropTypes from './reportPropTypes'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../components/withCurrentUserPersonalDetails'; +import styles from '../styles/styles'; +import roomAvatar from '../../assets/images/avatars/room.png'; +import * as ReportUtils from '../libs/ReportUtils'; +import MenuItem from '../components/MenuItem'; +import Clipboard from '../libs/Clipboard'; +import * as Expensicons from '../components/Icon/Expensicons'; +import getPlatform from '../libs/getPlatform'; +import CONST from '../CONST'; + +const propTypes = { + /** The report currently being looked at */ + report: reportPropTypes, + + ...withLocalizePropTypes, + ...withCurrentUserPersonalDetailsPropTypes, +}; + +const defaultProps = { + report: undefined, + ...withCurrentUserPersonalDetailsDefaultProps, +}; + +// eslint-disable-next-line react/prefer-stateless-function +class ShareCodePage extends React.Component { + qrCodeRef = React.createRef(); + + render() { + const isReport = this.props.report != null && this.props.report.reportID != null; + + const url = isReport ? `${CONST.NEW_EXPENSIFY_URL}r/${this.props.report.reportID}` : `${CONST.NEW_EXPENSIFY_URL}details?login=${this.props.session.email}`; + + const platform = getPlatform(); + const isNative = platform === CONST.PLATFORM.IOS || platform === CONST.PLATFORM.ANDROID; + + return ( + + Navigation.goBack()} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + + + + + + + + Clipboard.setString(url)} + /> + + {isNative && ( + this.qrCodeRef.current?.download()} + /> + )} + + + + ); + } +} + +ShareCodePage.propTypes = propTypes; +ShareCodePage.defaultProps = defaultProps; + +export default compose(withLocalize, withCurrentUserPersonalDetails)(ShareCodePage); diff --git a/src/pages/home/report/ReportDetailsShareCodePage.js b/src/pages/home/report/ReportDetailsShareCodePage.js new file mode 100644 index 000000000000..1dbfe25d2068 --- /dev/null +++ b/src/pages/home/report/ReportDetailsShareCodePage.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import reportPropTypes from '../../reportPropTypes'; +import ONYXKEYS from '../../../ONYXKEYS'; +import ShareCodePage from '../../ShareCodePage'; + +const propTypes = { + /** Navigation route context info provided by react navigation */ + route: PropTypes.shape({ + /** Route specific parameters used on this screen */ + params: PropTypes.shape({ + reportID: PropTypes.string, + }).isRequired, + }).isRequired, + + /** The report currently being looked at */ + report: reportPropTypes, +}; + +const defaultProps = { + report: undefined, +}; + +function ReportDetailsShareCodePage(props) { + return ; +} + +ReportDetailsShareCodePage.propTypes = propTypes; +ReportDetailsShareCodePage.defaultProps = defaultProps; + +export default withOnyx({ + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, + }, +})(ReportDetailsShareCodePage); diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 84ae9e30cd3c..eba3ccefb9fd 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -177,6 +177,13 @@ class InitialSettingsPage extends React.Component { const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(this.props.loginList); return [ + { + translationKey: 'common.shareCode', + icon: Expensicons.QrCode, + action: () => { + Navigation.navigate(ROUTES.SETTINGS_SHARE_CODE); + }, + }, { translationKey: 'common.workspaces', icon: Expensicons.Building, diff --git a/src/styles/styles.js b/src/styles/styles.js index cef8513b4cf5..c6b77c437b1c 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3224,10 +3224,27 @@ const styles = { contextMenuItemPopoverMaxWidth: { maxWidth: 375, }, + taskCheckbox: { height: 16, width: 16, }, + + shareCodePage: { + paddingHorizontal: 38.5, + }, + + shareCodeContainer: { + width: '100%', + alignItems: 'center', + paddingHorizontal: variables.qrShareHorizontalPadding, + paddingVertical: 20, + borderRadius: 20, + overflow: 'hidden', + borderColor: themeColors.borderFocus, + borderWidth: 2, + backgroundColor: themeColors.highlightBG, + }, }; export default styles; diff --git a/src/styles/variables.js b/src/styles/variables.js index 109c1dc410c3..33b98a0c4c08 100644 --- a/src/styles/variables.js +++ b/src/styles/variables.js @@ -136,4 +136,5 @@ export default { googleEmptyListViewHeight: 14, hoverDimValue: 0.5, pressDimValue: 0.8, + qrShareHorizontalPadding: 32, };