diff --git a/app.config.js b/app.config.js index 5351192..f08630e 100644 --- a/app.config.js +++ b/app.config.js @@ -29,6 +29,10 @@ export default { "applinks:hcb.hackclub.com", "applinks:bank.hackclub.com", ], + entitlements: { + "com.apple.developer.proximity-reader.payment.acceptance": [], + // I'm not entirely sure what to do here + } }, android: { icon: "./assets/app-icon.png", @@ -66,6 +70,15 @@ export default { "expo-local-authentication", { faceIDPermission: "Allow $(PRODUCT_NAME) to use Face ID." }, ], + [ + "@stripe/stripe-terminal-react-native", + { + "bluetoothBackgroundMode": true, + "locationWhenInUsePermission": "Location access is required in order to accept payments.", + "bluetoothPeripheralPermission": "Bluetooth access is required in order to connect to supported bluetooth card readers.", + "bluetoothAlwaysUsagePermission": "This app uses Bluetooth to connect to supported card readers." + } + ], ], }, }; diff --git a/package-lock.json b/package-lock.json index 2f12851..7a12b95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "@config-plugins/react-native-dynamic-app-icon": "^7.0.0", "@expo/vector-icons": "^14.0.0", "@react-native-async-storage/async-storage": "1.21.0", + "@react-native-community/geolocation": "^3.3.0", "@react-native-menu/menu": "^0.9.1", "@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/native": "^6.1.10", "@react-navigation/native-stack": "^6.9.13", + "@stripe/stripe-terminal-react-native": "^0.0.1-beta.20", "date-fns": "^2.30.0", "expo": "^50.0.8", "expo-auth-session": "~5.4.0", @@ -30,6 +32,7 @@ "expo-linear-gradient": "~12.7.2", "expo-linking": "~6.2.2", "expo-local-authentication": "~13.8.0", + "expo-location": "^17.0.1", "expo-notifications": "~0.27.6", "expo-secure-store": "~12.8.1", "expo-splash-screen": "~0.26.4", @@ -44,6 +47,7 @@ "react-native-dynamic-app-icon": "^1.1.0", "react-native-gesture-handler": "~2.14.0", "react-native-pager-view": "6.2.3", + "react-native-progress": "^5.0.1", "react-native-reanimated": "~3.6.2", "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", @@ -5259,6 +5263,19 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/@react-native-community/geolocation": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-3.3.0.tgz", + "integrity": "sha512-7DFeuotH7m7ImoXffN3TmlGSFn1XjvsaphPort0XZKipssYbdHiKhVVWG+jzisvDhcXikUc6nbUJgddVBL6RDg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native-menu/menu": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@react-native-menu/menu/-/menu-0.9.1.tgz", @@ -5763,6 +5780,90 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@stripe/stripe-terminal-react-native": { + "version": "0.0.1-beta.20", + "resolved": "https://registry.npmjs.org/@stripe/stripe-terminal-react-native/-/stripe-terminal-react-native-0.0.1-beta.20.tgz", + "integrity": "sha512-Bda5wIQPrpuawK0qEMlsdOGcgZ042mUjDTPdhi449YNtLjpiUrL2J2XP5PiDOJE3mZ/dmXD82JZy20p6mw1H4w==", + "license": "MIT", + "dependencies": { + "@testing-library/react-native": "^12.4.0", + "base-64": "^1.0.0", + "react-native-gradle-plugin": "^0.71.19" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/@stripe/stripe-terminal-react-native/node_modules/@testing-library/react-native": { + "version": "12.5.3", + "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-12.5.3.tgz", + "integrity": "sha512-wSaplzjx51OVJI7MU8Mi2kxwfW0dYETn3jqSVHxtIXmEnmlWXk6f69sEaBbzdp6iDzhFB5E6rDWveqf5V/ap2A==", + "license": "MIT", + "dependencies": { + "jest-matcher-utils": "^29.7.0", + "pretty-format": "^29.7.0", + "redent": "^3.0.0" + }, + "peerDependencies": { + "jest": ">=28.0.0", + "react": ">=16.8.0", + "react-native": ">=0.59", + "react-test-renderer": ">=16.8.0" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, + "node_modules/@stripe/stripe-terminal-react-native/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@stripe/stripe-terminal-react-native/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@stripe/stripe-terminal-react-native/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/@stripe/stripe-terminal-react-native/node_modules/react-test-renderer": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz", + "integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "react-is": "^18.3.1", + "react-shallow-renderer": "^16.15.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/@tsconfig/react-native": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@tsconfig/react-native/-/react-native-3.0.2.tgz", @@ -6745,6 +6846,12 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -7859,7 +7966,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -9187,6 +9293,15 @@ "expo": "*" } }, + "node_modules/expo-location": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-17.0.1.tgz", + "integrity": "sha512-m+OzotzlAXO3ZZ1uqW5GC25nXW868zN+ROyBA1V4VF6jGay1ZEs4URPglCVUDzZby2F5wt24cMzqDKw2IX6nRw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-manifests": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.13.2.tgz", @@ -10877,7 +10992,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -10892,7 +11006,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -10907,7 +11020,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10923,7 +11035,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -10934,14 +11045,12 @@ "node_modules/jest-diff/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==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/jest-diff/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -10950,7 +11059,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -10964,7 +11072,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "engines": { "node": ">=10" }, @@ -10975,14 +11082,12 @@ "node_modules/jest-diff/node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/jest-diff/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -11018,7 +11123,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", @@ -11033,7 +11137,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -11048,7 +11151,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -11064,7 +11166,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -11075,14 +11176,12 @@ "node_modules/jest-matcher-utils/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==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/jest-matcher-utils/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -11091,7 +11190,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -11105,7 +11203,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "engines": { "node": ">=10" }, @@ -11116,14 +11213,12 @@ "node_modules/jest-matcher-utils/node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/jest-matcher-utils/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -12883,6 +12978,15 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -14404,6 +14508,12 @@ "react-native": "*" } }, + "node_modules/react-native-gradle-plugin": { + "version": "0.71.19", + "resolved": "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.19.tgz", + "integrity": "sha512-1dVk9NwhoyKHCSxcrM6vY6cxmojeATsBobDicX0ZKr7DgUF2cBQRTKsimQFvzH8XhOVXyH8p4HyDSZNIFI8OlQ==", + "license": "MIT" + }, "node_modules/react-native-pager-view": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz", @@ -14413,6 +14523,18 @@ "react-native": "*" } }, + "node_modules/react-native-progress": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-native-progress/-/react-native-progress-5.0.1.tgz", + "integrity": "sha512-TYfJ4auAe5vubDma2yfFvt7ktSI+UCfysqJnkdHEcLXqAitRFOozgF/cLgN5VNi/iLdaf3ga1ETi2RF4jVZ/+g==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react-native-svg": "*" + } + }, "node_modules/react-native-reanimated": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.3.tgz", @@ -14643,6 +14765,19 @@ "node": ">=0.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -14942,9 +15077,10 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "peer": true, "dependencies": { "loose-envify": "^1.1.0" @@ -15484,6 +15620,18 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", diff --git a/package.json b/package.json index 581d810..8d14bb1 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,12 @@ "@config-plugins/react-native-dynamic-app-icon": "^7.0.0", "@expo/vector-icons": "^14.0.0", "@react-native-async-storage/async-storage": "1.21.0", + "@react-native-community/geolocation": "^3.3.0", "@react-native-menu/menu": "^0.9.1", "@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/native": "^6.1.10", "@react-navigation/native-stack": "^6.9.13", + "@stripe/stripe-terminal-react-native": "^0.0.1-beta.20", "date-fns": "^2.30.0", "expo": "^50.0.8", "expo-auth-session": "~5.4.0", @@ -34,6 +36,8 @@ "expo-image-picker": "~14.7.1", "expo-linear-gradient": "~12.7.2", "expo-linking": "~6.2.2", + "expo-local-authentication": "~13.8.0", + "expo-location": "^17.0.1", "expo-notifications": "~0.27.6", "expo-secure-store": "~12.8.1", "expo-splash-screen": "~0.26.4", @@ -48,14 +52,14 @@ "react-native-dynamic-app-icon": "^1.1.0", "react-native-gesture-handler": "~2.14.0", "react-native-pager-view": "6.2.3", + "react-native-progress": "^5.0.1", "react-native-reanimated": "~3.6.2", "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", "react-native-svg": "14.1.0", "react-native-web": "^0.19.8", "swr": "^2.2.1", - "ts-pattern": "^5.0.8", - "expo-local-authentication": "~13.8.0" + "ts-pattern": "^5.0.8" }, "devDependencies": { "@babel/core": "^7.22.11", diff --git a/src/Navigator.tsx b/src/Navigator.tsx index 450aed6..9931e46 100644 --- a/src/Navigator.tsx +++ b/src/Navigator.tsx @@ -28,6 +28,8 @@ import RenameTransactionPage from "./pages/RenameTransaction"; import SettingsPage from "./pages/Settings"; import TransactionPage from "./pages/Transaction"; import { palette } from "./theme"; +import OrganizationDonationPage from "./pages/organization/Donation"; +import ProcessDonationPage from "./pages/organization/ProcessDonation"; const Stack = createNativeStackNavigator(); const CardsStack = createNativeStackNavigator(); @@ -150,6 +152,19 @@ export default function Navigator() { title: "Manage Organization", }} /> + + { + initialize(); + }, [initialize]); + return ( + + ); +} + + +interface PaymentIntent { + id: string + amount: number + created: string + currency: string + sdkUuid: string + paymentMethodId: string +} + +export function PageStripe() { + const { location, accessDenied } = useLocation() + + const [value, setValue] = useState(0) + const [reader, setReader] = useState() + const [payment, setPayment] = useState() + const [loadingCreatePayment, setLoadingCreatePayment] = useState(false) + const [loadingCollectPayment, setLoadingCollectPayment] = useState(false) + const [loadingConfirmPayment, setLoadingConfirmPayment] = useState(false) + const [loadingConnectingReader, setLoadingConnectingReader] = useState(false) + + const locationIdStripeMock = 'tml_FrcFgksbiIZZ2V' + + const { + discoverReaders, + connectLocalMobileReader, + createPaymentIntent, + collectPaymentMethod, + confirmPaymentIntent, + connectedReader, + } = useStripeTerminal({ + onUpdateDiscoveredReaders: (readers: any) => { + setReader(readers[0]) + }, + }) + + useEffect(() => { + discoverReaders({ + discoveryMethod: 'localMobile', + simulated: true, + }) + }, [discoverReaders]) + + async function connectReader(selectedReader: any) { + setLoadingConnectingReader(true) + try { + const { reader, error } = await connectLocalMobileReader({ + reader: selectedReader, + locationId: locationIdStripeMock, + }) + + if (error) { + console.log('connectLocalMobileReader error:', error) + return + } + + Alert.alert('Reader connected successfully') + + console.log('Reader connected successfully', reader) + } catch (error) { + console.log(error) + } finally { + setLoadingConnectingReader(false) + } + } + + async function paymentIntent() { + setLoadingCreatePayment(true) + try { + const { error, paymentIntent } = await createPaymentIntent({ + amount: Number((value * 100).toFixed()), + currency: 'usd', + paymentMethodTypes: ['card_present'], + offlineBehavior: 'prefer_online', + }) + + if (error) { + console.log('Error creating payment intent', error) + return + } + + setPayment(paymentIntent) + + Alert.alert('Payment intent created successfully') + } catch (error) { + console.log(error) + } finally { + setLoadingCreatePayment(false) + } + } + + async function collectPayment() { + setLoadingCollectPayment(true) + try { + const { error, paymentIntent } = await collectPaymentMethod({ + paymentIntent: payment, + } as any) + + if (error) { + console.log('Error collecting payment', error) + Alert.alert('Error collecting payment', error.message) + return + } + + Alert.alert('Payment successfully collected', '', [ + { + text: 'Ok', + onPress: async () => { + console.log(paymentIntent); + await confirmPayment() + }, + }, + ]) + } catch (error) { + console.log(error) + } finally { + setLoadingCollectPayment(false) + } + } + + async function confirmPayment() { + setLoadingConfirmPayment(true) + console.log("foo", { payment }) + try { + const { error, paymentIntent } = await confirmPaymentIntent({ + paymentIntent: payment as any + }) + + if (error) { + console.log('Error confirm payment', error) + return + } + + Alert.alert('Payment successfully confirmed!', 'Congratulations') + console.log('Payment confirmed', paymentIntent) + } catch (error) { + console.log(error) + } finally { + setLoadingConfirmPayment(false) + } + } + + async function handleRequestLocation() { + await Linking.openSettings() + } + + useEffect(() => { + if (accessDenied) { + Alert.alert( + 'Access to location', + 'To use the app, you need to allow the use of your device location.', + [ + { + text: 'Activate', + onPress: handleRequestLocation, + }, + ] + ) + } + }, [accessDenied]) + + return ( + + + + + Stripe + + + + + + Amount to be charged + + setValue(Number(inputValue))} + keyboardType="numeric" + /> + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/src/lib/NavigatorParamList.ts b/src/lib/NavigatorParamList.ts index 83f35ca..f01079e 100644 --- a/src/lib/NavigatorParamList.ts +++ b/src/lib/NavigatorParamList.ts @@ -10,7 +10,9 @@ export type StackParamList = { Invitation: { inviteId: Invitation["id"]; invitation?: Invitation }; Event: { orgId: Organization["id"]; organization?: Organization }; AccountNumber: { orgId: Organization["id"] }; + ProcessDonation: { orgId: Organization["id"], payment: any }; OrganizationSettings: { orgId: Organization["id"] }; + OrganizationDonation: { orgId: Organization["id"] }; Transaction: { transactionId: Transaction["id"]; orgId?: Organization["id"]; diff --git a/src/lib/useLocation.ts b/src/lib/useLocation.ts new file mode 100644 index 0000000..89e2cba --- /dev/null +++ b/src/lib/useLocation.ts @@ -0,0 +1,90 @@ +import { useCallback, useState } from 'react' +import * as Location from 'expo-location' +import { useFocusEffect } from '@react-navigation/native' +import { PermissionsAndroid, Platform } from 'react-native' +import Geolocation from '@react-native-community/geolocation' + +export function useLocation() { + const [accessDenied, setAccessDenied] = useState(false) + const [location, setLocation] = useState(null) + + async function requestLocationPermission() { + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, + { + title: 'Localização', + message: 'Permitir que o aplicativo utilize a sua localização.', + buttonPositive: 'OK', + } + ) + + return granted === PermissionsAndroid.RESULTS.GRANTED + } catch (err) { + console.error(err) + } + } + + const getAndroidLocation = useCallback(async () => { + const granted = await requestLocationPermission() + + if (!granted) { + setAccessDenied(true) + return + } + + Geolocation.getCurrentPosition( + (position: any) => { + const coordinates = { + latitude: position.coords.latitude.toString(), + longitude: position.coords.longitude.toString(), + } + + setLocation(coordinates) + }, + (error: any) => { + console.error('Error getting location:', error) + }, + { enableHighAccuracy: true } + ) + }, []) + + const getIosLocation = useCallback(async () => { + const { status } = await Location.requestForegroundPermissionsAsync() + + if (status !== 'granted') { + setAccessDenied(true) + return + } + const location = await Location.getCurrentPositionAsync() + + const coordinates = { + latitude: location.coords.latitude.toString(), + longitude: location.coords.longitude.toString(), + } + + setLocation(coordinates) + }, []) + + useFocusEffect( + useCallback(() => { + const getLocation = async () => { + if (Platform.OS === 'android') { + await getAndroidLocation() + return + } + + await getIosLocation() + } + + getLocation().catch((err) => { + console.error(err) + }) + }, [getAndroidLocation, getIosLocation]) + ) + + return { + accessDenied, + location, + } +} \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 74b0253..0c59560 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -17,6 +17,7 @@ import { } from "react-native"; import useSWR, { preload, useSWRConfig } from "swr"; +import Stripe from "../components/Stripe"; import Transaction from "../components/Transaction"; import { StackParamList } from "../lib/NavigatorParamList"; import usePinnedOrgs from "../lib/organization/usePinnedOrgs"; @@ -27,6 +28,7 @@ import ITransaction from "../lib/types/Transaction"; import { palette } from "../theme"; import { orgColor, renderMoney } from "../util"; + function EventBalance({ balance_cents }: { balance_cents?: number }) { return balance_cents !== undefined ? ( @@ -223,6 +225,8 @@ export default function App({ navigation }: Props) { const tabBarHeight = useBottomTabBarHeight(); const scheme = useColorScheme(); + + useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion preload("user", fetcher!); @@ -262,116 +266,112 @@ export default function App({ navigation }: Props) { } return ( - - {organizations && ( - { - // mutate( - // (key: string) => - // key?.startsWith("/organizations/") || - // key == "/user/organizations", - // ); - // }} - ListHeaderComponent={() => - invitations && - invitations.length > 0 && ( - - + {organizations && ( + { + // mutate( + // (key: string) => + // key?.startsWith("/organizations/") || + // key == "/user/organizations", + // ); + // }} + ListHeaderComponent={() => + invitations && + invitations.length > 0 && ( + - Pending invitations - - {invitations.map((invitation) => ( - - navigation.navigate("Invitation", { - inviteId: invitation.id, - invitation, - }) - } - hideBalance - /> - // - // - // {invitation.organization.name} - // - // - ))} - - ) - } - renderItem={({ item: organization }) => ( - - navigation.navigate("Event", { - orgId: organization.id, - organization, - }) - } - onHold={() => { - Haptics.notificationAsync( - Haptics.NotificationFeedbackType.Success, - ); - togglePinnedOrg(organization.id); - }} - /> - )} - ListFooterComponent={() => - organizations.length > 2 && ( - + Pending invitations + + {invitations.map((invitation) => ( + + navigation.navigate("Invitation", { + inviteId: invitation.id, + invitation, + }) + } + hideBalance + /> + // + // + // {invitation.organization.name} + // + // + ))} + + ) + } + renderItem={({ item: organization }) => ( + + navigation.navigate("Event", { + orgId: organization.id, + organization, + }) + } + onHold={() => { + Haptics.notificationAsync( + Haptics.NotificationFeedbackType.Success, + ); + togglePinnedOrg(organization.id); }} - > - Tap and hold to pin an organization - - ) - } - /> - )} - + /> + )} + ListFooterComponent={() => +<> + + + + + } + /> + )} + + ); } diff --git a/src/pages/organization/Donation.tsx b/src/pages/organization/Donation.tsx new file mode 100644 index 0000000..62ef704 --- /dev/null +++ b/src/pages/organization/Donation.tsx @@ -0,0 +1,395 @@ +import * as Progress from 'react-native-progress'; +import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs"; +import { useTheme } from "@react-navigation/native"; +import { NativeStackScreenProps } from "@react-navigation/native-stack"; +import { formatDistanceToNowStrict, parseISO } from "date-fns"; +import { capitalize } from "lodash"; +import { ActivityIndicator, Linking, ScrollView, Text, View } from "react-native"; +import useSWR, { useSWRConfig } from "swr"; +import { StripeTerminalProvider } from '@stripe/stripe-terminal-react-native'; + +import Stripe from "../../components/Stripe"; +import Button from "../../components/Button"; +import UserAvatar from "../../components/UserAvatar"; +import { StackParamList } from "../../lib/NavigatorParamList"; +import { OrganizationExpanded } from "../../lib/types/Organization"; +import User, { OrgUser } from "../../lib/types/User"; +import { palette } from "../../theme"; + +import { useEffect, useState } from 'react' +import { + Alert, + ImageBackground, + SafeAreaView, + TextInput, +} from 'react-native' +import { useStripeTerminal } from '@stripe/stripe-terminal-react-native' +import { useLocation } from "../../lib/useLocation"; + +interface PaymentIntent { + id: string + amount: number + created: string + currency: string + sdkUuid: string + paymentMethodId: string +} + +type Props = NativeStackScreenProps; + +export default function OrganizationDonationPage({ + route: { + params: { orgId }, + }, + navigation +}: Props) { + const { fetcher } = useSWRConfig(); + + const fetchTokenProvider = async () => { + console.log("Fetching stripe token") + + const result = await fetcher!("user"); + + console.log(result); + + const token = (result as any).terminal_connection_token; + console.log(token) + + return token.secret; + }; + + return ( + + + + ); +} + +function PageWrapper({ orgId, navigation }: any) { + + const { initialize, isInitialized, connectLocalMobileReader } = useStripeTerminal({ + + }); + useEffect(() => { + initialize(); + }, [initialize]); + + if (!isInitialized) return ( + + Connecting... + + + ); + + return ; +} + +const SectionHeader = ({ title, subtitle }: { title: string, subtitle?: string }) => { + const { colors } = useTheme(); + + return ( + <> + + {title} + + {subtitle && ( + {subtitle} + )} + + ); +}; + +function PageContent({ orgId, navigation }: any) { + const { colors } = useTheme(); + + // const { data: organization } = useSWR( + // `organizations/${orgId}?avatar_size=50`, + // { fallbackData: cache.get(`organizations/${orgId}`)?.data }, + // ); + const { data: currentUser } = useSWR("user"); + + const tabBarHeight = useBottomTabBarHeight(); + const { colors: themeColors } = useTheme(); + + // if (!organization) return null; + + const { location, accessDenied } = useLocation() + + const [amount, setAmount] = useState(""); + + const value = parseFloat(amount); + + const [reader, setReader] = useState() + const [payment, setPayment] = useState() + const [loadingCreatePayment, setLoadingCreatePayment] = useState(false) + const [loadingCollectPayment, setLoadingCollectPayment] = useState(false) + const [loadingConfirmPayment, setLoadingConfirmPayment] = useState(false) + const [loadingConnectingReader, setLoadingConnectingReader] = useState(false) + const [currentProgress, setCurrentProgress] = useState(null) + + const locationIdStripeMock = 'tml_FrcFgksbiIZZ2V' + + const { + discoverReaders, + connectLocalMobileReader, + createPaymentIntent, + collectPaymentMethod, + confirmPaymentIntent, + connectedReader, + } = useStripeTerminal({ + onUpdateDiscoveredReaders: (readers: any) => { + setReader(readers[0]) + }, + onDidReportReaderSoftwareUpdateProgress: (progress: any) => { + setCurrentProgress(progress); + } + }) + + console.log("discovery", discoverReaders) + + useEffect(() => { + discoverReaders({ + discoveryMethod: 'localMobile', + simulated: false + }); + }, [discoverReaders]) + + async function connectReader(selectedReader: any) { + setLoadingConnectingReader(true) + try { + const { reader, error } = await connectLocalMobileReader({ + reader: selectedReader, + locationId: locationIdStripeMock, + }); + + setCurrentProgress(null); + + if (error) { + console.log('connectLocalMobileReader error:', error) + if (error.message == "You must provide a reader object") { + discoverReaders({ + discoveryMethod: "localMobile", + simulated: false, + }); + } + Alert.alert('Error connecting, please try again') + return + } + + setCurrentProgress(null); + + console.log('Reader connected successfully', reader) + } catch (error) { + console.log(error) + } finally { + setLoadingConnectingReader(false) + } + } + + async function paymentIntent() { + setLoadingCreatePayment(true) + try { + const { error, paymentIntent } = await createPaymentIntent({ + amount: Number((value * 100).toFixed()), + currency: 'usd', + paymentMethodTypes: ['card_present'], + offlineBehavior: 'prefer_online', + captureMethod: "automatic" + }) + + if (error) { + console.log('Error creating payment intent', error) + return + } + + setPayment(paymentIntent) + + navigation.navigate("ProcessDonation", { + orgId, + payment: paymentIntent, + collectPayment: () => collectPayment(paymentIntent) + }); + + return paymentIntent + } catch (error) { + console.log(error) + Alert.alert('Error creating payment intent', error.message) + } finally { + setLoadingCreatePayment(false) + } + } + + async function collectPayment(localPayment: any) { + setLoadingCollectPayment(true) + console.log(localPayment) + let output; + try { + const { error, paymentIntent } = await collectPaymentMethod({ + paymentIntent: localPayment, + } as any) + + if (error) { + console.log('Error collecting payment', error) + Alert.alert('Error collecting payment', error.message) + return + } + output = await confirmPayment(localPayment) + } catch (error) { + console.log(error) + output = false + } finally { + setLoadingCollectPayment(false) + } + + return output + } + + async function confirmPayment(localPayment: any) { + setLoadingConfirmPayment(true) + let success + try { + const { error, paymentIntent } = await confirmPaymentIntent({ + paymentIntent: localPayment as any + }) + if (error) { + console.log('Error confirm payment', error) + return + } + console.log('Payment confirmed', paymentIntent) + setPayment(undefined) + success = true + } catch (error) { + console.log(error) + success = false + } finally { + setLoadingConfirmPayment(false) + } + return success + } + + async function handleRequestLocation() { + await Linking.openSettings() + } + + useEffect(() => { + if (accessDenied) { + Alert.alert( + 'Access to location', + 'To use the app, you need to allow the use of your device location.', + [ + { + text: 'Activate', + onPress: handleRequestLocation, + }, + ] + ) + } + }, [accessDenied]) + + if (!connectedReader) { + // centered view that says "connect reader" + return ( + + Collect donations + + {loadingConnectingReader && !currentProgress && } + {currentProgress && } + + ) + } + + return ( + + + + Donation amount + + { + const stripped = inputValue.split("").filter(char => "1234567890.".includes(char)).join(""); + const formatted = stripped.split(".").length > 2 ? stripped.slice(0, -1) : stripped; + const capped = formatted.indexOf(".") >= 0 ? formatted.substring(0, formatted.indexOf(".") + 3) : formatted; + + setAmount(capped); + }} + keyboardType="numeric" + /> + + {connectedReader ? ( + + ) : ( + + )} + + + ); +} diff --git a/src/pages/organization/ProcessDonation.tsx b/src/pages/organization/ProcessDonation.tsx new file mode 100644 index 0000000..ae8566a --- /dev/null +++ b/src/pages/organization/ProcessDonation.tsx @@ -0,0 +1,197 @@ +import { Ionicons } from "@expo/vector-icons"; +import { useTheme } from "@react-navigation/native"; +import { NativeStackScreenProps } from "@react-navigation/native-stack"; +import * as Clipboard from "expo-clipboard"; +import { useEffect, useState } from "react"; +import { View, Text, StatusBar, Button, ActivityIndicator } from "react-native"; +import useSWR from "swr"; + +import StyledButton from "../../components/Button"; +import { StackParamList } from "../../lib/NavigatorParamList"; +import { OrganizationExpanded } from "../../lib/types/Organization"; +import { palette } from "../../theme"; + +type Props = NativeStackScreenProps; + +const SectionHeader = ({ title, subtitle }: { title: string, subtitle?: string }) => { + const { colors } = useTheme(); + + return ( + <> + + {title} + + {subtitle && ( + {subtitle} + )} + + ); +}; + +function Stat({ + title, + value, +}: { + title: string; + value: string | undefined; +}) { + const { colors: themeColors } = useTheme(); + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (copied) { + const timeout = setTimeout(() => setCopied(false), 2000); + return () => clearTimeout(timeout); + } + }, [copied]); + + return ( + + {title} + + + {value} + + + + ); +} + +export default function ProcessDonationPage({ + navigation, + route: { + params: { orgId, payment, collectPayment }, + }, +}: Props) { + const { data: organization } = useSWR( + `organizations/${orgId}`, + ); + + const [status, setStatus] = useState<"ready" | "loading" | "success" | "error">("ready"); + + useEffect(() => { + navigation.setOptions({ + headerLeft: () => ( +