Replace apollo with tanstack query
This commit is contained in:
12 files changed
+135
-326
No files matched your search
Generated
+3
-191
@@ -8,7 +8,6 @@
|
|||||||
"name": "cnapp",
|
"name": "cnapp",
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.14.1",
|
|
||||||
"@date-fns/tz": "^1.5.0",
|
"@date-fns/tz": "^1.5.0",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"@react-native-vector-icons/feather": "^13.1.2",
|
"@react-native-vector-icons/feather": "^13.1.2",
|
||||||
@@ -31,7 +30,6 @@
|
|||||||
"expo-notifications": "~57.0.8",
|
"expo-notifications": "~57.0.8",
|
||||||
"expo-splash-screen": "~57.0.5",
|
"expo-splash-screen": "~57.0.5",
|
||||||
"expo-status-bar": "~57.0.1",
|
"expo-status-bar": "~57.0.1",
|
||||||
"graphql": "^16.9.0",
|
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.52.1",
|
||||||
"react-native": "0.86.2",
|
"react-native": "0.86.2",
|
||||||
@@ -60,48 +58,6 @@
|
|||||||
"typescript": "~6.0.3"
|
"typescript": "~6.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@apollo/client": {
|
|
||||||
"version": "3.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.14.1.tgz",
|
|
||||||
"integrity": "sha512-SgGX6E23JsZhUdG2anxiyHvEvvN6CUaI4ZfMsndZFeuHPXL3H0IsaiNAhLITSISbeyeYd+CBd9oERXQDdjXWZw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@graphql-typed-document-node/core": "^3.1.1",
|
|
||||||
"@wry/caches": "^1.0.0",
|
|
||||||
"@wry/equality": "^0.5.6",
|
|
||||||
"@wry/trie": "^0.5.0",
|
|
||||||
"graphql-tag": "^2.12.6",
|
|
||||||
"hoist-non-react-statics": "^3.3.2",
|
|
||||||
"optimism": "^0.18.0",
|
|
||||||
"prop-types": "^15.7.2",
|
|
||||||
"rehackt": "^0.1.0",
|
|
||||||
"symbol-observable": "^4.0.0",
|
|
||||||
"ts-invariant": "^0.10.3",
|
|
||||||
"tslib": "^2.3.0",
|
|
||||||
"zen-observable-ts": "^1.2.5"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"graphql": "^15.0.0 || ^16.0.0",
|
|
||||||
"graphql-ws": "^5.5.5 || ^6.0.3",
|
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc",
|
|
||||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc",
|
|
||||||
"subscriptions-transport-ws": "^0.9.0 || ^0.11.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"graphql-ws": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"subscriptions-transport-ws": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
@@ -2076,15 +2032,6 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@graphql-typed-document-node/core": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||||
@@ -4912,54 +4859,6 @@
|
|||||||
"integrity": "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==",
|
"integrity": "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/@wry/caches": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@wry/context": {
|
|
||||||
"version": "0.7.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz",
|
|
||||||
"integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@wry/equality": {
|
|
||||||
"version": "0.5.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz",
|
|
||||||
"integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@wry/trie": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@xmldom/xmldom": {
|
"node_modules/@xmldom/xmldom": {
|
||||||
"version": "0.8.13",
|
"version": "0.8.13",
|
||||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||||
@@ -8899,30 +8798,6 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/graphql": {
|
|
||||||
"version": "16.14.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz",
|
|
||||||
"integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/graphql-tag": {
|
|
||||||
"version": "2.12.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.7.tgz",
|
|
||||||
"integrity": "sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-bigints": {
|
"node_modules/has-bigints": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
||||||
@@ -12488,6 +12363,7 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -12669,18 +12545,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/optimism": {
|
|
||||||
"version": "0.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz",
|
|
||||||
"integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@wry/caches": "^1.0.0",
|
|
||||||
"@wry/context": "^0.7.0",
|
|
||||||
"@wry/trie": "^0.5.0",
|
|
||||||
"tslib": "^2.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -13264,6 +13128,7 @@
|
|||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
@@ -13275,6 +13140,7 @@
|
|||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
@@ -13937,24 +13803,6 @@
|
|||||||
"regjsparser": "bin/parser"
|
"regjsparser": "bin/parser"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rehackt": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz",
|
|
||||||
"integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"react": "*"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@@ -15015,15 +14863,6 @@
|
|||||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
"node_modules/symbol-observable": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/symbol-tree": {
|
"node_modules/symbol-tree": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
@@ -15260,18 +15099,6 @@
|
|||||||
"typescript": ">=4.8.4"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-invariant": {
|
|
||||||
"version": "0.10.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz",
|
|
||||||
"integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.15.0",
|
"version": "3.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||||
@@ -16112,21 +15939,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zen-observable": {
|
|
||||||
"version": "0.8.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
|
|
||||||
"integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/zen-observable-ts": {
|
|
||||||
"version": "1.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz",
|
|
||||||
"integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"zen-observable": "0.8.15"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
"web": "expo start --web"
|
"web": "expo start --web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.14.1",
|
|
||||||
"@date-fns/tz": "^1.5.0",
|
"@date-fns/tz": "^1.5.0",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"@react-native-vector-icons/feather": "^13.1.2",
|
"@react-native-vector-icons/feather": "^13.1.2",
|
||||||
@@ -38,7 +37,6 @@
|
|||||||
"expo-notifications": "~57.0.8",
|
"expo-notifications": "~57.0.8",
|
||||||
"expo-splash-screen": "~57.0.5",
|
"expo-splash-screen": "~57.0.5",
|
||||||
"expo-status-bar": "~57.0.1",
|
"expo-status-bar": "~57.0.1",
|
||||||
"graphql": "^16.9.0",
|
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-hook-form": "^7.52.1",
|
"react-hook-form": "^7.52.1",
|
||||||
"react-native": "0.86.2",
|
"react-native": "0.86.2",
|
||||||
|
|||||||
+8
-16
@@ -1,4 +1,3 @@
|
|||||||
import { ApolloClient, HttpLink, InMemoryCache, ApolloProvider } from '@apollo/client';
|
|
||||||
import { StripeProvider } from '@stripe/stripe-react-native';
|
import { StripeProvider } from '@stripe/stripe-react-native';
|
||||||
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
@@ -24,11 +23,6 @@ Notifications.setNotificationHandler({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const apolloClient = new ApolloClient({
|
|
||||||
link: new HttpLink({ uri: `${process.env.EXPO_PUBLIC_DUSKEN_API_URL}/api/graphql/` }),
|
|
||||||
cache: new InMemoryCache(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
useDeepLinking();
|
useDeepLinking();
|
||||||
return (
|
return (
|
||||||
@@ -36,16 +30,14 @@ export default function App() {
|
|||||||
<StatusBar style="light" />
|
<StatusBar style="light" />
|
||||||
<PersistQueryClientProvider client={appQueryClient} persistOptions={persistOptions}>
|
<PersistQueryClientProvider client={appQueryClient} persistOptions={persistOptions}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ApolloProvider client={apolloClient}>
|
{/* FIXME: Read urlScheme and merchantIdentifier from .Config */}
|
||||||
{/* FIXME: Read urlScheme and merchantIdentifier from .Config */}
|
<StripeProvider
|
||||||
<StripeProvider
|
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''}
|
||||||
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''}
|
urlScheme={Constants.expoConfig?.scheme as string}
|
||||||
urlScheme={Constants.expoConfig?.scheme as string}
|
merchantIdentifier="merchant.no.neuf.chateau" // https://developer.apple.com/account/resources/identifiers/list/merchant
|
||||||
merchantIdentifier="merchant.no.neuf.chateau" // https://developer.apple.com/account/resources/identifiers/list/merchant
|
>
|
||||||
>
|
<AppInner />
|
||||||
<AppInner />
|
</StripeProvider>
|
||||||
</StripeProvider>
|
|
||||||
</ApolloProvider>
|
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</PersistQueryClientProvider>
|
</PersistQueryClientProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
|
|||||||
+38
-1
@@ -1,4 +1,12 @@
|
|||||||
import { deleteUser, fetchEvents, fetchUserData, login, registerUser, requestStripePaymentSheet } from '@/api';
|
import {
|
||||||
|
deleteUser,
|
||||||
|
fetchEvents,
|
||||||
|
fetchMembershipTypes,
|
||||||
|
fetchUserData,
|
||||||
|
login,
|
||||||
|
registerUser,
|
||||||
|
requestStripePaymentSheet,
|
||||||
|
} from '@/api';
|
||||||
import { ApiError } from '@/errors';
|
import { ApiError } from '@/errors';
|
||||||
|
|
||||||
const fetchMock = global.fetch as jest.Mock;
|
const fetchMock = global.fetch as jest.Mock;
|
||||||
@@ -172,6 +180,35 @@ describe('fetchEvents', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('fetchMembershipTypes', () => {
|
||||||
|
it('posts the GraphQL query and resolves the membership types', async () => {
|
||||||
|
const membershipTypes = [{ slug: 'regular', price: 30000 }];
|
||||||
|
mockResponseOnce({ data: { membershipTypes } });
|
||||||
|
|
||||||
|
await expect(fetchMembershipTypes()).resolves.toEqual(membershipTypes);
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
'https://dusken.example.test/api/graphql/',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ query: '{ membershipTypes { slug price } }' }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when the API reports a GraphQL error', async () => {
|
||||||
|
mockResponseOnce({ data: null, errors: [{ message: 'Cannot query field "membershipTypes"' }] });
|
||||||
|
|
||||||
|
await expect(fetchMembershipTypes()).rejects.toThrow('Kunne ikke hente priser');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on a non-ok response', async () => {
|
||||||
|
mockResponseOnce({ data: { membershipTypes: [] } }, false, 500);
|
||||||
|
|
||||||
|
await expect(fetchMembershipTypes()).rejects.toThrow('Kunne ikke hente priser');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('requestStripePaymentSheet', () => {
|
describe('requestStripePaymentSheet', () => {
|
||||||
it('posts the membership type and returns the payment sheet parameters', async () => {
|
it('posts the membership type and returns the payment sheet parameters', async () => {
|
||||||
const sheet = { paymentIntent: 'pi_secret', ephemeralKey: 'ek_secret', customer: 'cus_123' };
|
const sheet = { paymentIntent: 'pi_secret', ephemeralKey: 'ek_secret', customer: 'cus_123' };
|
||||||
|
|||||||
+15
-1
@@ -1,5 +1,5 @@
|
|||||||
import { ApiError } from '@/errors';
|
import { ApiError } from '@/errors';
|
||||||
import type { EventType, UserType } from '@/types/data';
|
import type { EventType, MembershipTypeOption, UserType } from '@/types/data';
|
||||||
import type { UserRegisterFormValues } from '@/types/forms';
|
import type { UserRegisterFormValues } from '@/types/forms';
|
||||||
import { fetchWithTimeout } from '@/utils';
|
import { fetchWithTimeout } from '@/utils';
|
||||||
|
|
||||||
@@ -115,6 +115,20 @@ export async function fetchEvents(): Promise<EventType[]> {
|
|||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchMembershipTypes(): Promise<MembershipTypeOption[]> {
|
||||||
|
const response = await fetchWithTimeout(`${process.env.EXPO_PUBLIC_DUSKEN_API_URL}/api/graphql/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: JSON_HEADERS,
|
||||||
|
body: JSON.stringify({ query: '{ membershipTypes { slug price } }' }),
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
const membershipTypes = response.ok ? json?.data?.membershipTypes : null;
|
||||||
|
if (!membershipTypes) {
|
||||||
|
throw new Error('Kunne ikke hente priser');
|
||||||
|
}
|
||||||
|
return membershipTypes;
|
||||||
|
}
|
||||||
|
|
||||||
/** Create a stripe payment sheet; rejects with a message for the failure toast. */
|
/** Create a stripe payment sheet; rejects with a message for the failure toast. */
|
||||||
export async function requestStripePaymentSheet(authToken: string, membershipType: string) {
|
export async function requestStripePaymentSheet(authToken: string, membershipType: string) {
|
||||||
const response = await fetchWithTimeout(`${process.env.EXPO_PUBLIC_DUSKEN_API_URL}/api/stripe/payment-sheet/`, {
|
const response = await fetchWithTimeout(`${process.env.EXPO_PUBLIC_DUSKEN_API_URL}/api/stripe/payment-sheet/`, {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
import { fireEvent } from '@testing-library/react-native';
|
import { fireEvent } from '@testing-library/react-native';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RefreshControl } from 'react-native';
|
import { RefreshControl } from 'react-native';
|
||||||
|
|
||||||
import Membership from '@/modules/membership/Membership';
|
import Membership from '@/modules/membership/Membership';
|
||||||
import { renderWithProviders, testUser } from '@/testUtils';
|
import { mockMembershipTypesOnce, renderWithProviders, testUser } from '@/testUtils';
|
||||||
import type { UserType } from '@/types/data';
|
import type { UserType } from '@/types/data';
|
||||||
|
|
||||||
const mockNavigate = jest.fn();
|
const mockNavigate = jest.fn();
|
||||||
@@ -13,25 +12,6 @@ jest.mock('@react-navigation/native', () => ({
|
|||||||
useNavigation: () => ({ navigate: mockNavigate }),
|
useNavigation: () => ({ navigate: mockNavigate }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Must match PROOF_QUERY in useProof.tsx
|
|
||||||
const PROOF_QUERY = gql`
|
|
||||||
{
|
|
||||||
membershipTypes {
|
|
||||||
slug
|
|
||||||
price
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const apolloMocks = [
|
|
||||||
{
|
|
||||||
request: { query: PROOF_QUERY },
|
|
||||||
result: {
|
|
||||||
data: { membershipTypes: [{ __typename: 'MembershipType', slug: 'standard', price: 25000 }] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it('shows the membership pitch when logged out', async () => {
|
it('shows the membership pitch when logged out', async () => {
|
||||||
const { findByText } = renderWithProviders(<Membership />);
|
const { findByText } = renderWithProviders(<Membership />);
|
||||||
|
|
||||||
@@ -42,23 +22,10 @@ it('shows the membership pitch when logged out', async () => {
|
|||||||
it('lets a logged in non-member pick a membership type and shows its price', async () => {
|
it('lets a logged in non-member pick a membership type and shows its price', async () => {
|
||||||
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
||||||
|
|
||||||
|
mockMembershipTypesOnce();
|
||||||
const { findByText, getByText } = renderWithProviders(<Membership />, {
|
const { findByText, getByText } = renderWithProviders(<Membership />, {
|
||||||
initialToken: 'test-token',
|
initialToken: 'test-token',
|
||||||
initialUser: nonMember,
|
initialUser: nonMember,
|
||||||
apolloMocks: [
|
|
||||||
{
|
|
||||||
request: { query: PROOF_QUERY },
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
membershipTypes: [
|
|
||||||
{ __typename: 'MembershipType', slug: 'student', price: 15000 },
|
|
||||||
{ __typename: 'MembershipType', slug: 'regular', price: 30000 },
|
|
||||||
{ __typename: 'MembershipType', slug: 'support', price: 50000 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// All three membership types are offered
|
// All three membership types are offered
|
||||||
@@ -79,10 +46,10 @@ it('lets a logged in non-member pick a membership type and shows its price', asy
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows the membership proof when logged in', async () => {
|
it('shows the membership proof when logged in', async () => {
|
||||||
|
mockMembershipTypesOnce();
|
||||||
const { findByText } = renderWithProviders(<Membership />, {
|
const { findByText } = renderWithProviders(<Membership />, {
|
||||||
initialToken: 'test-token',
|
initialToken: 'test-token',
|
||||||
initialUser: testUser,
|
initialUser: testUser,
|
||||||
apolloMocks,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await findByText('MEDLEM')).toBeTruthy();
|
expect(await findByText('MEDLEM')).toBeTruthy();
|
||||||
@@ -90,15 +57,16 @@ it('shows the membership proof when logged in', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('fetches the user with the stored token when only the token survives a cold start', async () => {
|
it('fetches the user with the stored token when only the token survives a cold start', async () => {
|
||||||
(global.fetch as jest.Mock).mockResolvedValue({
|
// The user fetch resolves first; Proof then mounts and fetches membership types
|
||||||
|
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
json: async () => testUser,
|
json: async () => testUser,
|
||||||
});
|
});
|
||||||
|
mockMembershipTypesOnce();
|
||||||
|
|
||||||
const { findByText } = renderWithProviders(<Membership />, {
|
const { findByText } = renderWithProviders(<Membership />, {
|
||||||
initialToken: 'test-token',
|
initialToken: 'test-token',
|
||||||
apolloMocks,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await findByText('MEDLEM')).toBeTruthy();
|
expect(await findByText('MEDLEM')).toBeTruthy();
|
||||||
@@ -116,10 +84,10 @@ it('refetches and renders updated user data on pull-to-refresh', async () => {
|
|||||||
const fetchMock = global.fetch as jest.Mock;
|
const fetchMock = global.fetch as jest.Mock;
|
||||||
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
||||||
|
|
||||||
|
mockMembershipTypesOnce();
|
||||||
const { findByText, UNSAFE_getByType } = renderWithProviders(<Membership />, {
|
const { findByText, UNSAFE_getByType } = renderWithProviders(<Membership />, {
|
||||||
initialToken: 'test-token',
|
initialToken: 'test-token',
|
||||||
initialUser: nonMember,
|
initialUser: nonMember,
|
||||||
apolloMocks,
|
|
||||||
});
|
});
|
||||||
expect(await findByText('Velg medlemskapstype')).toBeTruthy();
|
expect(await findByText('Velg medlemskapstype')).toBeTruthy();
|
||||||
|
|
||||||
@@ -146,17 +114,16 @@ it('signs out when the stored token has been revoked', async () => {
|
|||||||
|
|
||||||
const { findByText } = renderWithProviders(<Membership />, {
|
const { findByText } = renderWithProviders(<Membership />, {
|
||||||
initialToken: 'revoked-token',
|
initialToken: 'revoked-token',
|
||||||
apolloMocks,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await findByText('Bli medlem')).toBeTruthy();
|
expect(await findByText('Bli medlem')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs out and shows the logged-out membership screen again', async () => {
|
it('logs out and shows the logged-out membership screen again', async () => {
|
||||||
|
mockMembershipTypesOnce();
|
||||||
const { findByText } = renderWithProviders(<Membership />, {
|
const { findByText } = renderWithProviders(<Membership />, {
|
||||||
initialToken: 'test-token',
|
initialToken: 'test-token',
|
||||||
initialUser: testUser,
|
initialUser: testUser,
|
||||||
apolloMocks,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.press(await findByText('Logg ut'));
|
fireEvent.press(await findByText('Logg ut'));
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import { fireEvent, waitFor } from '@testing-library/react-native';
|
import { fireEvent, waitFor } from '@testing-library/react-native';
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Proof from '@/modules/membership/Proof';
|
import Proof from '@/modules/membership/Proof';
|
||||||
import { renderWithProviders, testUser } from '@/testUtils';
|
import { mockMembershipTypesOnce, renderWithProviders, testUser } from '@/testUtils';
|
||||||
import type { UserType } from '@/types/data';
|
import type { UserType } from '@/types/data';
|
||||||
|
|
||||||
// The setup-wide stripe mock resolves initPaymentSheet/presentPaymentSheet
|
// The setup-wide stripe mock resolves initPaymentSheet/presentPaymentSheet
|
||||||
@@ -24,31 +22,6 @@ const mockPaymentSheetResponse = () =>
|
|||||||
json: async () => ({ paymentIntent: 'pi_secret', ephemeralKey: 'ek_secret', customer: 'cus_123' }),
|
json: async () => ({ paymentIntent: 'pi_secret', ephemeralKey: 'ek_secret', customer: 'cus_123' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Must match PROOF_QUERY in useProof.tsx
|
|
||||||
const PROOF_QUERY = gql`
|
|
||||||
{
|
|
||||||
membershipTypes {
|
|
||||||
slug
|
|
||||||
price
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const apolloMocks = [
|
|
||||||
{
|
|
||||||
request: { query: PROOF_QUERY },
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
membershipTypes: [
|
|
||||||
{ __typename: 'MembershipType', slug: 'student', price: 15000 },
|
|
||||||
{ __typename: 'MembershipType', slug: 'regular', price: 30000 },
|
|
||||||
{ __typename: 'MembershipType', slug: 'support', price: 50000 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
||||||
|
|
||||||
const expiredMember: UserType = {
|
const expiredMember: UserType = {
|
||||||
@@ -57,19 +30,18 @@ const expiredMember: UserType = {
|
|||||||
last_membership: { membership_type: 'standard', end_date: '2020-01-01' },
|
last_membership: { membership_type: 'standard', end_date: '2020-01-01' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderProof = ({
|
// The membership type query fires on mount, so its response must be queued
|
||||||
user = nonMember,
|
// before render; later mockResolvedValueOnce calls queue up behind it.
|
||||||
fetchUser = jest.fn(),
|
const renderProof = ({ user = nonMember, fetchUser = jest.fn() }: { user?: UserType; fetchUser?: () => void } = {}) => {
|
||||||
mocks = apolloMocks,
|
mockMembershipTypesOnce();
|
||||||
}: { user?: UserType; fetchUser?: () => void; mocks?: readonly MockedResponse[] } = {}) =>
|
return renderWithProviders(
|
||||||
renderWithProviders(
|
|
||||||
<Proof onLogoutPress={jest.fn()} fetchUser={fetchUser} isRefetching={false} onDeletePress={jest.fn()} />,
|
<Proof onLogoutPress={jest.fn()} fetchUser={fetchUser} isRefetching={false} onDeletePress={jest.fn()} />,
|
||||||
{
|
{
|
||||||
initialToken: 'test-token',
|
initialToken: 'test-token',
|
||||||
initialUser: user,
|
initialUser: user,
|
||||||
apolloMocks: mocks,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -87,9 +59,8 @@ it('disables the purchase button without showing a price while membership types
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows an error with retry instead of a price when membership types fail to load', async () => {
|
it('shows an error with retry instead of a price when membership types fail to load', async () => {
|
||||||
const { findByText, getByText, getByRole } = renderProof({
|
fetchMock.mockRejectedValueOnce(new Error('network down'));
|
||||||
mocks: [{ request: { query: PROOF_QUERY }, error: new Error('network down') }, ...apolloMocks],
|
const { findByText, getByText, getByRole } = renderProof();
|
||||||
});
|
|
||||||
|
|
||||||
expect(await findByText('Kunne ikke hente priser')).toBeTruthy();
|
expect(await findByText('Kunne ikke hente priser')).toBeTruthy();
|
||||||
expect(getByRole('button', { name: 'Kjøp medlemskap' })).toBeDisabled();
|
expect(getByRole('button', { name: 'Kjøp medlemskap' })).toBeDisabled();
|
||||||
@@ -101,8 +72,8 @@ it('shows an error with retry instead of a price when membership types fail to l
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('purchases the preselected membership through the stripe payment sheet', async () => {
|
it('purchases the preselected membership through the stripe payment sheet', async () => {
|
||||||
mockPaymentSheetResponse();
|
|
||||||
const { findByText } = renderProof();
|
const { findByText } = renderProof();
|
||||||
|
mockPaymentSheetResponse();
|
||||||
|
|
||||||
// 'Ordinær' (slug: regular) is preselected; the button enables once the
|
// 'Ordinær' (slug: regular) is preselected; the button enables once the
|
||||||
// membership type query has resolved and its price is shown
|
// membership type query has resolved and its price is shown
|
||||||
@@ -132,8 +103,8 @@ it('purchases the preselected membership through the stripe payment sheet', asyn
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('purchases the selected membership type, not the default', async () => {
|
it('purchases the selected membership type, not the default', async () => {
|
||||||
mockPaymentSheetResponse();
|
|
||||||
const { findByText } = renderProof();
|
const { findByText } = renderProof();
|
||||||
|
mockPaymentSheetResponse();
|
||||||
|
|
||||||
fireEvent.press(await findByText('Student'));
|
fireEvent.press(await findByText('Student'));
|
||||||
fireEvent.press(await findByText('Kjøp medlemskap (150 NOK)'));
|
fireEvent.press(await findByText('Kjøp medlemskap (150 NOK)'));
|
||||||
@@ -146,11 +117,11 @@ it('purchases the selected membership type, not the default', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows the stripe error when the payment sheet is not completed', async () => {
|
it('shows the stripe error when the payment sheet is not completed', async () => {
|
||||||
mockPaymentSheetResponse();
|
|
||||||
presentPaymentSheet.mockResolvedValueOnce({
|
presentPaymentSheet.mockResolvedValueOnce({
|
||||||
error: { code: 'Canceled', message: 'The payment flow has been canceled' },
|
error: { code: 'Canceled', message: 'The payment flow has been canceled' },
|
||||||
});
|
});
|
||||||
const { findByText, queryByText } = renderProof();
|
const { findByText, queryByText } = renderProof();
|
||||||
|
mockPaymentSheetResponse();
|
||||||
|
|
||||||
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
||||||
|
|
||||||
@@ -160,12 +131,12 @@ it('shows the stripe error when the payment sheet is not completed', async () =>
|
|||||||
|
|
||||||
it('shows an error and does not present the sheet when the backend rejects the purchase', async () => {
|
it('shows an error and does not present the sheet when the backend rejects the purchase', async () => {
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const { findByText, getByRole } = renderProof();
|
||||||
fetchMock.mockResolvedValueOnce({
|
fetchMock.mockResolvedValueOnce({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 400,
|
status: 400,
|
||||||
json: async () => ({ detail: 'Ugyldig medlemskapstype' }),
|
json: async () => ({ detail: 'Ugyldig medlemskapstype' }),
|
||||||
});
|
});
|
||||||
const { findByText, getByRole } = renderProof();
|
|
||||||
|
|
||||||
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
||||||
|
|
||||||
@@ -178,9 +149,9 @@ it('shows an error and does not present the sheet when the backend rejects the p
|
|||||||
|
|
||||||
it('shows an error and does not present the sheet when stripe fails to initialise it', async () => {
|
it('shows an error and does not present the sheet when stripe fails to initialise it', async () => {
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
mockPaymentSheetResponse();
|
|
||||||
initPaymentSheet.mockResolvedValueOnce({ error: { code: 'Failed', message: 'Invalid client secret' } });
|
initPaymentSheet.mockResolvedValueOnce({ error: { code: 'Failed', message: 'Invalid client secret' } });
|
||||||
const { findByText } = renderProof();
|
const { findByText } = renderProof();
|
||||||
|
mockPaymentSheetResponse();
|
||||||
|
|
||||||
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
import { gql, useQuery } from '@apollo/client';
|
|
||||||
import { useStripe } from '@stripe/stripe-react-native';
|
import { useStripe } from '@stripe/stripe-react-native';
|
||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useNotifications } from '@/hooks/useNotifications';
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
|
import { useMembershipTypes } from '@/queries/useMembershipTypes';
|
||||||
import { usePaymentSheetMutation } from '@/queries/usePaymentSheetMutation';
|
import { usePaymentSheetMutation } from '@/queries/usePaymentSheetMutation';
|
||||||
import { useUser } from '@/queries/useUser';
|
import { useUser } from '@/queries/useUser';
|
||||||
import { ToastProps } from '@/types/toast';
|
import { ToastProps } from '@/types/toast';
|
||||||
|
|
||||||
const PROOF_QUERY = gql`
|
|
||||||
{
|
|
||||||
membershipTypes {
|
|
||||||
slug
|
|
||||||
price
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface UseProofProps {
|
interface UseProofProps {
|
||||||
fetchUser: () => void;
|
fetchUser: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useProof = ({ fetchUser }: UseProofProps) => {
|
const useProof = ({ fetchUser }: UseProofProps) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { data, loading, error, refetch } = useQuery(PROOF_QUERY);
|
const { data: membershipTypes, isPending, isError, refetch } = useMembershipTypes();
|
||||||
const { initPaymentSheet, presentPaymentSheet } = useStripe();
|
const { initPaymentSheet, presentPaymentSheet } = useStripe();
|
||||||
const { mutateAsync: createPaymentSheet } = usePaymentSheetMutation();
|
const { mutateAsync: createPaymentSheet } = usePaymentSheetMutation();
|
||||||
const { scheduleMembershipExpiryNotification, hasPermission } = useNotifications();
|
const { scheduleMembershipExpiryNotification, hasPermission } = useNotifications();
|
||||||
@@ -35,9 +26,7 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
|||||||
// null until the query has delivered a price for the selected type; the
|
// null until the query has delivered a price for the selected type; the
|
||||||
// purchase button stays disabled rather than showing a guessed amount
|
// purchase button stays disabled rather than showing a guessed amount
|
||||||
const membershipPrice =
|
const membershipPrice =
|
||||||
data?.membershipTypes?.find(
|
membershipTypes?.find((membershipTypeOption) => membershipTypeOption.slug === membershipTypeSlug)?.price ?? null;
|
||||||
(membershipTypeOption: { slug: string; price: number }) => membershipTypeOption.slug === membershipTypeSlug,
|
|
||||||
)?.price ?? null;
|
|
||||||
|
|
||||||
const openPaymentSheet = async () => {
|
const openPaymentSheet = async () => {
|
||||||
setPaymentLoading(true);
|
setPaymentLoading(true);
|
||||||
@@ -92,9 +81,9 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
|||||||
}, [membershipType, membershipEndDate, fetchUser, hasPermission, scheduleMembershipExpiryNotification]);
|
}, [membershipType, membershipEndDate, fetchUser, hasPermission, scheduleMembershipExpiryNotification]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoadingMembershipType: loading,
|
isLoadingMembershipType: isPending,
|
||||||
membershipPrice: membershipPrice != null ? membershipPrice / 100 : null,
|
membershipPrice: membershipPrice != null ? membershipPrice / 100 : null,
|
||||||
membershipTypesError: !!error,
|
membershipTypesError: isError,
|
||||||
refetchMembershipTypes: refetch,
|
refetchMembershipTypes: refetch,
|
||||||
onPurchasePress: openPaymentSheet,
|
onPurchasePress: openPaymentSheet,
|
||||||
paymentLoading,
|
paymentLoading,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const queryKeys = {
|
export const queryKeys = {
|
||||||
user: ['user'],
|
user: ['user'],
|
||||||
events: ['events'],
|
events: ['events'],
|
||||||
|
membershipTypes: ['membershipTypes'],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { fetchMembershipTypes } from '@/api';
|
||||||
|
import { queryKeys } from '@/queries/queryKeys';
|
||||||
|
|
||||||
|
/** The purchasable membership types with prices. */
|
||||||
|
export function useMembershipTypes() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: queryKeys.membershipTypes,
|
||||||
|
queryFn: fetchMembershipTypes,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
+24
-15
@@ -1,6 +1,3 @@
|
|||||||
import { InMemoryCache } from '@apollo/client';
|
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { render } from '@testing-library/react-native';
|
import { render } from '@testing-library/react-native';
|
||||||
@@ -9,7 +6,7 @@ import type { ReactElement } from 'react';
|
|||||||
|
|
||||||
import { AuthProvider } from '@/auth/AuthContext';
|
import { AuthProvider } from '@/auth/AuthContext';
|
||||||
import { queryKeys } from '@/queries/queryKeys';
|
import { queryKeys } from '@/queries/queryKeys';
|
||||||
import type { UserType } from '@/types/data';
|
import type { MembershipTypeOption, UserType } from '@/types/data';
|
||||||
|
|
||||||
/** A member with an active membership, for screens that require a logged in user. */
|
/** A member with an active membership, for screens that require a logged in user. */
|
||||||
export const testUser: UserType = {
|
export const testUser: UserType = {
|
||||||
@@ -25,22 +22,36 @@ export const testUser: UserType = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** The purchase options offered in the app, as served by /api/graphql/. */
|
||||||
|
export const testMembershipTypes: MembershipTypeOption[] = [
|
||||||
|
{ slug: 'student', price: 15000 },
|
||||||
|
{ slug: 'regular', price: 30000 },
|
||||||
|
{ slug: 'support', price: 50000 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Queue a successful /api/graphql/ membership types response on the fetch mock. */
|
||||||
|
export function mockMembershipTypesOnce(membershipTypes: MembershipTypeOption[] = testMembershipTypes) {
|
||||||
|
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({ data: { membershipTypes } }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
interface RenderWithProvidersOptions {
|
interface RenderWithProvidersOptions {
|
||||||
/** Auth token the app has stored; being set means the user is logged in. */
|
/** Auth token the app has stored; being set means the user is logged in. */
|
||||||
initialToken?: string | null;
|
initialToken?: string | null;
|
||||||
/** Already-fetched user data, as after a completed /api/me/ request. */
|
/** Already-fetched user data, as after a completed /api/me/ request. */
|
||||||
initialUser?: UserType | null;
|
initialUser?: UserType | null;
|
||||||
apolloMocks?: readonly MockedResponse[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a screen inside the providers it expects in the real app: a fresh
|
* Renders a screen inside the providers it expects in the real app: a fresh
|
||||||
* query client (no persister), AuthProvider, Apollo's MockedProvider and a
|
* query client (no persister), AuthProvider and a NavigationContainer.
|
||||||
* NavigationContainer.
|
|
||||||
*/
|
*/
|
||||||
export function renderWithProviders(
|
export function renderWithProviders(
|
||||||
ui: ReactElement,
|
ui: ReactElement,
|
||||||
{ initialToken = null, initialUser = null, apolloMocks = [] }: RenderWithProvidersOptions = {},
|
{ initialToken = null, initialUser = null }: RenderWithProvidersOptions = {},
|
||||||
) {
|
) {
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -55,13 +66,11 @@ export function renderWithProviders(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = render(
|
const result = render(
|
||||||
<MockedProvider mocks={apolloMocks} cache={new InMemoryCache()}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<AuthProvider initialToken={initialToken}>
|
||||||
<AuthProvider initialToken={initialToken}>
|
<NavigationContainer>{ui}</NavigationContainer>
|
||||||
<NavigationContainer>{ui}</NavigationContainer>
|
</AuthProvider>
|
||||||
</AuthProvider>
|
</QueryClientProvider>,
|
||||||
</QueryClientProvider>
|
|
||||||
</MockedProvider>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return { queryClient, ...result };
|
return { queryClient, ...result };
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export interface MembershipType {
|
|||||||
end_date: string;
|
end_date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A purchasable membership type; price is in øre. */
|
||||||
|
export interface MembershipTypeOption {
|
||||||
|
slug: string;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserType {
|
export interface UserType {
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
|
|||||||
Reference in new issue
Block a user