From 3cdb65ddfa4b26a0ceb0c4bbd31d9a21f52c2efc Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Tue, 19 Sep 2023 16:12:58 +0200 Subject: [PATCH] chore: print SDK error contents (#77) --- Makefile | 3 +- README.md | 10 +- app.config.js | 2 +- app.json | 4 +- contrib/sdk/typescript.yml | 2 + openapitools.json | 2 +- package-lock.json | 168 +++++++++++++++---------- package.json | 7 +- src/components/ErrorBoundary.tsx | 6 +- src/components/Ory/Ui/Node/Input.tsx | 6 +- src/components/Routes/Callback.tsx | 6 +- src/components/Routes/Home.tsx | 32 ++--- src/components/Routes/Login.tsx | 16 ++- src/components/Routes/Recovery.tsx | 7 +- src/components/Routes/Registration.tsx | 20 ++- src/components/Routes/Settings.tsx | 31 +++-- src/components/Routes/Verification.tsx | 5 +- src/helpers/axios.tsx | 50 +++++++- src/helpers/form.tsx | 81 +++++++++--- src/helpers/sdk.tsx | 6 +- src/translations.ts | 49 -------- 21 files changed, 310 insertions(+), 203 deletions(-) delete mode 100644 src/translations.ts diff --git a/Makefile b/Makefile index eef552f..c3521f5 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,7 @@ build-sdk: --git-repo-id sdk \ --git-host github.com \ -c ./contrib/sdk/typescript.yml - (cd ./contrib/sdk/generated; npm i; npm run build) + (cd ./contrib/sdk/generated; npm i; npm run build; npm link) make format npm i + npm link @ory/client diff --git a/README.md b/README.md index 282174d..24dab76 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# ORY Kratos React Native Self-Service UI Reference +# Ory Kratos React Native Self-Service UI Reference This is an exemplary Self Service UI for -[ORY Kratos](https://github.com/ory/kratos) Self Service features: +[Ory Kratos](https://github.com/ory/kratos) Self Service features: - Registration - Login @@ -20,12 +20,12 @@ There is an excellent write-up available on implementing [Mobile Login with Username / Email and Password in React Native](https://www.ory.sh/react-native-authentication-login-signup/) but if you want to go ahead and just try it out, do the following: -1. Run the [ORY Kratos quickstart](http://ory.sh/docs/kratos/quickstart) on your +1. Run the [Ory Kratos quickstart](http://ory.sh/docs/kratos/quickstart) on your local machine. -1. Use [`ngrok`](http://ngrok.com) to expose the ORY Kratos public port to the +1. Use [`ngrok`](http://ngrok.com) to expose the Ory Kratos public port to the public internet (`ngrok http 4433`). 1. Install this project's dependencies with `npm i`. 1. Use the resulting URL and start the environment using `KRATOS_URL=https://.ngrok.io npm start`. Please note that the Web Interface is not working currently due to security features implemented - in ORY Kratos. + in Ory Kratos. diff --git a/app.config.js b/app.config.js index a67711c..e930cde 100644 --- a/app.config.js +++ b/app.config.js @@ -4,7 +4,7 @@ export default (parent = {}) => { const { env = {} } = process || {} const { - // This is the URL of your deployment. In our case we use the ORY Demo + // This is the URL of your deployment. In our case we use the Ory Demo // environment KRATOS_URL = "https://playground.projects.oryapis.com", diff --git a/app.json b/app.json index 861bd58..8866604 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,6 @@ { "expo": { - "name": "ORY Profile App", + "name": "Ory Profile App", "slug": "kratos-selfservice-ui-react-native", "scheme": "kratos-selfservice-ui-react-native", "version": "0.5.14", @@ -30,7 +30,7 @@ "favicon": "./assets/favicon.png" }, "platforms": ["ios", "android", "web"], - "description": "An example app demonstrating login, registration, profile settings, 2fa, account recovery, and more using the ORY Kratos open source project.", + "description": "An example app demonstrating login, registration, profile settings, 2fa, account recovery, and more using the Ory Kratos open source project.", "plugins": ["sentry-expo"], "hooks": { "postPublish": [ diff --git a/contrib/sdk/typescript.yml b/contrib/sdk/typescript.yml index ef041f1..a7fbc1d 100644 --- a/contrib/sdk/typescript.yml +++ b/contrib/sdk/typescript.yml @@ -1,7 +1,9 @@ npmName: "@ory/client" npmVersion: v0.0.1 +# typescriptThreePlus: true supportsES6: true ensureUniqueParams: true modelPropertyNaming: original disallowAdditionalPropertiesIfNotPresent: false +withInterfaces: false useSingleRequestParameter: true diff --git a/openapitools.json b/openapitools.json index 0436938..9cbc6d5 100644 --- a/openapitools.json +++ b/openapitools.json @@ -2,6 +2,6 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "6.4.0" + "version": "5.4.0" } } diff --git a/package-lock.json b/package-lock.json index 88dfc9b..12945c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,19 @@ "dependencies": { "@expo-google-fonts/roboto": "0.2.3", "@expo-google-fonts/rubik": "0.2.3", - "@expo/webpack-config": "^19.0.0", - "@ory/client": "1.1.46", + "@expo/webpack-config": "19.0.0", + "@ory/client": "1.2.4", "@ory/themes": "0.0.21", "@react-native-async-storage/async-storage": "1.18.2", "@react-native-community/async-storage": "1.12.1", "@react-native-community/masked-view": "0.1.11", "@react-navigation/native": "6.1.7", "@react-navigation/stack": "6.3.17", + "@redtea/format-axios-error": "2.1.1", "@sentry/integrations": "7.52.0", "@sentry/react-native": "5.5.0", "@types/node": "18.17.5", - "axios": "1.4.0", + "axios": "0.21.4", "expo": "49.0.10", "expo-application": "5.3.0", "expo-auth-session": "5.0.2", @@ -4162,21 +4163,13 @@ } }, "node_modules/@ory/client": { - "version": "1.1.46", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-1.1.46.tgz", - "integrity": "sha512-+GK2Jo3g4gK/JY8XiZNp3blOQlGWlqCFVP8bYNrJ52erGvmYZQYabTXle/fFnwAuKhEn1ADH3kqi9nISye4rcg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@ory/client/-/client-1.2.4.tgz", + "integrity": "sha512-iA3HTzzNJtsotIAKO9Jo4eeZs4ElXnI3F0p7iqZhMq5rGubWxRv2SC+vLgGOhDevkzsVI2UXYAbkXxloUxai5A==", "dependencies": { "axios": "^0.21.4" } }, - "node_modules/@ory/client/node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, "node_modules/@ory/themes": { "version": "0.0.21", "resolved": "https://registry.npmjs.org/@ory/themes/-/themes-0.0.21.tgz", @@ -6279,6 +6272,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/@redtea/format-axios-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@redtea/format-axios-error/-/format-axios-error-2.1.1.tgz", + "integrity": "sha512-bGfqYctnX/+nBuHsOo6RDP8JrHoetnf6BEK5+v1lmbVg5w9kvt/UmKg+5YlT3YCj4UQZX86LdN6cZF083A9fyA==", + "dependencies": { + "logform": "2.2.0" + } + }, "node_modules/@segment/loosely-validate-event": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", @@ -7547,26 +7548,11 @@ } }, "node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" + "follow-redirects": "^1.14.0" } }, "node_modules/babel-core": { @@ -8646,6 +8632,14 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -10840,6 +10834,11 @@ "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, "node_modules/fast-xml-parser": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.7.tgz", @@ -10933,6 +10932,11 @@ "node": "*" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "node_modules/fetch-retry": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-4.1.1.tgz", @@ -13358,6 +13362,18 @@ "node": ">=4" } }, + "node_modules/logform": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", + "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, "node_modules/logkitty": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", @@ -18566,6 +18582,14 @@ "node": ">=0.6" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -22596,21 +22620,11 @@ } }, "@ory/client": { - "version": "1.1.46", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-1.1.46.tgz", - "integrity": "sha512-+GK2Jo3g4gK/JY8XiZNp3blOQlGWlqCFVP8bYNrJ52erGvmYZQYabTXle/fFnwAuKhEn1ADH3kqi9nISye4rcg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@ory/client/-/client-1.2.4.tgz", + "integrity": "sha512-iA3HTzzNJtsotIAKO9Jo4eeZs4ElXnI3F0p7iqZhMq5rGubWxRv2SC+vLgGOhDevkzsVI2UXYAbkXxloUxai5A==", "requires": { "axios": "^0.21.4" - }, - "dependencies": { - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - } } }, "@ory/themes": { @@ -24112,6 +24126,14 @@ } } }, + "@redtea/format-axios-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@redtea/format-axios-error/-/format-axios-error-2.1.1.tgz", + "integrity": "sha512-bGfqYctnX/+nBuHsOo6RDP8JrHoetnf6BEK5+v1lmbVg5w9kvt/UmKg+5YlT3YCj4UQZX86LdN6cZF083A9fyA==", + "requires": { + "logform": "2.2.0" + } + }, "@segment/loosely-validate-event": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", @@ -25209,25 +25231,11 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" }, "axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "requires": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } + "follow-redirects": "^1.14.0" } }, "babel-core": { @@ -26041,6 +26049,11 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -27678,6 +27691,11 @@ "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==" }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, "fast-xml-parser": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.7.tgz", @@ -27744,6 +27762,11 @@ "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==" }, + "fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "fetch-retry": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-4.1.1.tgz", @@ -29504,6 +29527,18 @@ "chalk": "^2.0.1" } }, + "logform": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", + "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, "logkitty": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", @@ -33392,6 +33427,11 @@ "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", "dev": true }, + "triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==" + }, "ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/package.json b/package.json index 0b94b65..9678d4b 100644 --- a/package.json +++ b/package.json @@ -23,18 +23,19 @@ "dependencies": { "@expo-google-fonts/roboto": "0.2.3", "@expo-google-fonts/rubik": "0.2.3", - "@expo/webpack-config": "^19.0.0", - "@ory/client": "1.1.46", + "@expo/webpack-config": "19.0.0", + "@ory/client": "1.2.4", "@ory/themes": "0.0.21", "@react-native-async-storage/async-storage": "1.18.2", "@react-native-community/async-storage": "1.12.1", "@react-native-community/masked-view": "0.1.11", "@react-navigation/native": "6.1.7", "@react-navigation/stack": "6.3.17", + "@redtea/format-axios-error": "2.1.1", "@sentry/integrations": "7.52.0", "@sentry/react-native": "5.5.0", "@types/node": "18.17.5", - "axios": "1.4.0", + "axios": "0.21.4", "expo": "49.0.10", "expo-application": "5.3.0", "expo-auth-session": "5.0.2", diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 803e80b..345bd12 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,13 +1,11 @@ -import React, { Component, ErrorInfo } from "react" +import React, { Component, ErrorInfo, PropsWithChildren } from "react" import { Text } from "react-native" -interface Props {} - interface State { hasError: boolean } -export default class ErrorBoundary extends Component { +export default class ErrorBoundary extends Component { state = { hasError: false, } diff --git a/src/components/Ory/Ui/Node/Input.tsx b/src/components/Ory/Ui/Node/Input.tsx index 177bf52..9ecabd3 100644 --- a/src/components/Ory/Ui/Node/Input.tsx +++ b/src/components/Ory/Ui/Node/Input.tsx @@ -85,21 +85,21 @@ export const NodeInput = ({ let extraProps: TextInputProps = {} switch (variant) { case "email": - extraProps.autoCompleteType = "email" + extraProps.autoComplete = "email" extraProps.keyboardType = "email-address" extraProps.textContentType = "emailAddress" extraProps.autoCapitalize = "none" extraProps.autoCorrect = false break case "password": - extraProps.autoCompleteType = "password" + extraProps.autoComplete = "password" extraProps.textContentType = "password" extraProps.autoCapitalize = "none" extraProps.secureTextEntry = true extraProps.autoCorrect = false break case "username": - extraProps.autoCompleteType = "username" + extraProps.autoComplete = "username" extraProps.textContentType = "username" extraProps.autoCapitalize = "none" extraProps.autoCorrect = false diff --git a/src/components/Routes/Callback.tsx b/src/components/Routes/Callback.tsx index a8b4fa4..0f85474 100644 --- a/src/components/Routes/Callback.tsx +++ b/src/components/Routes/Callback.tsx @@ -1,12 +1,14 @@ import { StackScreenProps } from "@react-navigation/stack" import * as WebBrowser from "expo-web-browser" -import React from "react" +import React, { useEffect } from "react" import { RootStackParamList } from "../Navigation" type Props = StackScreenProps +// This is the route that the user is redirected to after they have logged in with their OIDC (SSO) account. +// It will handle the completion of the auth request const Callback = (props: Props) => { - React.useEffect(() => { + useEffect(() => { WebBrowser.maybeCompleteAuthSession({ skipRedirectCheck: false, }) diff --git a/src/components/Routes/Home.tsx b/src/components/Routes/Home.tsx index 6801254..fde0232 100644 --- a/src/components/Routes/Home.tsx +++ b/src/components/Routes/Home.tsx @@ -1,18 +1,20 @@ +import { StackScreenProps } from "@react-navigation/stack" import React, { useContext, useEffect } from "react" -import StyledText from "../Styled/StyledText" -import CodeBox from "../Styled/CodeBox" import { AuthContext } from "../AuthProvider" import Layout from "../Layout/Layout" +import { RootStackParamList } from "../Navigation" +import CodeBox from "../Styled/CodeBox" import StyledCard from "../Styled/StyledCard" -import { useNavigation } from "@react-navigation/native" +import StyledText from "../Styled/StyledText" -const Home = () => { - const navigation = useNavigation() +type Props = StackScreenProps + +const Home = ({ navigation }: Props) => { const { isAuthenticated, session, sessionToken } = useContext(AuthContext) useEffect(() => { if (!isAuthenticated || !session) { - navigation.navigate("Login") + navigation.navigate("Login", {}) } }, [isAuthenticated, sessionToken]) @@ -20,10 +22,10 @@ const Home = () => { return null } - // Get the name, or if it does not exist in the traits, use the - // identity's ID - const { name: { first = String(session.identity.id) } = {} } = session - .identity.traits as any + const traits = session.identity?.traits + + // Use the first name, the email, or the ID as the name + const first = traits.name?.first || traits.email || session.identity?.id return ( @@ -34,16 +36,14 @@ const Home = () => { Hello, nice to have you! You signed up with this data: - - {JSON.stringify(session.identity.traits || "{}", null, 2)} - + {JSON.stringify(traits || "{}", null, 2)} - You are signed in using an ORY Kratos Session Token: + You are signed in using an Ory Session Token: {sessionToken} - This app makes REST requests to ORY Kratos' Public API to validate and - decode the ORY Kratos Session payload: + This app makes REST requests to Ory Identities' Public API to validate + and decode the Ory Session payload: {JSON.stringify(session || "{}", null, 2)} diff --git a/src/components/Routes/Login.tsx b/src/components/Routes/Login.tsx index 4bd5a66..47b9a9b 100644 --- a/src/components/Routes/Login.tsx +++ b/src/components/Routes/Login.tsx @@ -5,6 +5,7 @@ import { StackScreenProps } from "@react-navigation/stack" import React, { useContext, useState } from "react" import { SessionContext } from "../../helpers/auth" +import { logSDKError } from "../../helpers/axios" import { handleFormSubmitError } from "../../helpers/form" import { newOrySdk } from "../../helpers/sdk" import { AuthContext } from "../AuthProvider" @@ -16,6 +17,7 @@ import { ProjectContext } from "../ProjectProvider" import AuthSubTitle from "../Styled/AuthSubTitle" import NavigationCard from "../Styled/NavigationCard" import StyledCard from "../Styled/StyledCard" +import * as AuthSession from "expo-auth-session" type Props = StackScreenProps @@ -30,17 +32,25 @@ const Login = ({ navigation, route }: Props) => { aal: route.params.aal, refresh: route.params.refresh, xSessionToken: sessionToken, - returnTo: "http://localhost:19006/Callback", + // If you do use social sign in, please add the following URLs to your allowed return to URLs. + // If you the app is running on an emulator or physical device: exp://localhost:8081 + // If you are using the web version: http://localhost:19006 (or whatever port you are using) + // If that does not work, please see the documentation of makeRedirectURI for more information: https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturioptions + // If you don't use Social sign in, you can comment out the following line. + returnTo: AuthSession.makeRedirectUri({ + preferLocalhost: true, + path: "/Callback", + }), returnSessionTokenExchangeCode: true, }) .then(({ data: f }) => setFlow(f)) - .catch(console.error) + .catch(logSDKError) const refetchFlow = () => newOrySdk(project) .getLoginFlow({ id: flow!.id }) .then(({ data: f }) => setFlow({ ...flow, ...f })) // merging ensures we don't lose the code - .catch(console.error) + .catch(logSDKError) // When the component is mounted, we initialize a new use login flow: useFocusEffect( diff --git a/src/components/Routes/Recovery.tsx b/src/components/Routes/Recovery.tsx index 417068f..af6bbd4 100644 --- a/src/components/Routes/Recovery.tsx +++ b/src/components/Routes/Recovery.tsx @@ -11,8 +11,9 @@ import AuthSubTitle from "../Styled/AuthSubTitle" import StyledCard from "../Styled/StyledCard" import { AuthContext } from "../AuthProvider" import NavigationCard from "../Styled/NavigationCard" -import { isAxiosError } from "axios" import { handleFormSubmitError } from "../../helpers/form" +import { logSDKError } from "../../helpers/axios" +import axios from "axios" type Props = StackScreenProps @@ -44,7 +45,7 @@ export default function Recovery({ navigation }: Props) { useFocusEffect( useCallback(() => { - initializeFlow().then(setFlow) + initializeFlow().then(setFlow).catch(logSDKError) return () => { setFlow(undefined) @@ -80,7 +81,7 @@ export default function Recovery({ navigation }: Props) { setFlow(updatedFlow) } } catch (err: unknown) { - if (isAxiosError(err)) { + if (axios.isAxiosError(err)) { handleFormSubmitError( flow, setFlow, diff --git a/src/components/Routes/Registration.tsx b/src/components/Routes/Registration.tsx index bc38584..e9d8e0f 100644 --- a/src/components/Routes/Registration.tsx +++ b/src/components/Routes/Registration.tsx @@ -16,6 +16,8 @@ import { ProjectContext } from "../ProjectProvider" import AuthSubTitle from "../Styled/AuthSubTitle" import NavigationCard from "../Styled/NavigationCard" import StyledCard from "../Styled/StyledCard" +import * as AuthSession from "expo-auth-session" +import { logSDKError } from "../../helpers/axios" type Props = StackScreenProps @@ -27,7 +29,15 @@ const Registration = ({ navigation }: Props) => { const initializeFlow = () => newOrySdk(project) .createNativeRegistrationFlow({ - returnTo: "http://localhost:19006/Callback", + // If you do use social sign in, please add the following URLs to your allowed return to URLs. + // If you the app is running on an emulator or physical device: exp://localhost:8081 + // If you are using the web version: http://localhost:19006 (or whatever port you are using) + // If that does not work, please see the documentation of makeRedirectURI for more information: https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturioptions + // If you don't use Social sign in, you can comment out the following line. + returnTo: AuthSession.makeRedirectUri({ + preferLocalhost: true, + path: "/Callback", + }), returnSessionTokenExchangeCode: true, }) // The flow was initialized successfully, let's set the form data: @@ -35,7 +45,7 @@ const Registration = ({ navigation }: Props) => { setFlow(flow) console.log("Setting registration flow", flow) }) - .catch(console.error) + .catch(logSDKError) // When the component is mounted, we initialize a new use login flow: useFocusEffect( @@ -54,7 +64,7 @@ const Registration = ({ navigation }: Props) => { newOrySdk(project) .getRegistrationFlow({ id: flow!.id }) .then(({ data: f }) => setFlow({ ...flow, ...f })) // merging ensures we don't lose the code - .catch(console.error) + .catch(logSDKError) const setSessionAndRedirect = (session: SessionContext) => { setSession(session) @@ -77,12 +87,12 @@ const Registration = ({ navigation }: Props) => { updateRegistrationFlowBody: payload, }) .then(({ data }) => { - // ORY Kratos can be configured in such a way that it requires a login after + // Ory Kratos can be configured in such a way that it requires a login after // registration. You could handle that case by navigating to the Login screen // but for simplicity we'll just print an error here: if (!data.session_token || !data.session) { const err = new Error( - "It looks like you configured ORY Kratos to not issue a session automatically after registration. This edge-case is currently not supported in this example app. You can find more information on enabling this feature here: https://www.ory.sh/kratos/docs/next/self-service/flows/user-registration#successful-registration", + "It looks like you configured Ory Idnetities to not issue a session automatically after registration. This edge-case is currently not supported in this example app. You can find more information on enabling this feature here: https://www.ory.sh/kratos/docs/next/self-service/flows/user-registration#successful-registration", ) return Promise.reject(err) } diff --git a/src/components/Routes/Settings.tsx b/src/components/Routes/Settings.tsx index c3cb812..6974c5b 100644 --- a/src/components/Routes/Settings.tsx +++ b/src/components/Routes/Settings.tsx @@ -6,20 +6,19 @@ import { SettingsFlowState, UpdateSettingsFlowBody, } from "@ory/client" -import { useNavigation, useRoute } from "@react-navigation/native" +import { StackScreenProps } from "@react-navigation/stack" import React, { useContext, useEffect, useState } from "react" import { showMessage } from "react-native-flash-message" import styled from "styled-components/native" +import { logSDKError } from "../../helpers/axios" import { handleFormSubmitError } from "../../helpers/form" -import { newOrySdk } from "../../helpers/sdk" import { AuthContext } from "../AuthProvider" import Layout from "../Layout/Layout" +import { RootStackParamList } from "../Navigation" import { SelfServiceFlow } from "../Ory/Ui" import { ProjectContext } from "../ProjectProvider" import StyledCard from "../Styled/StyledCard" import StyledText from "../Styled/StyledText" -import { StackScreenProps } from "@react-navigation/stack" -import { RootStackParamList } from "../Navigation" const CardTitle = styled.View` margin-bottom: 15px; @@ -58,9 +57,11 @@ const Settings = ({ navigation, route }: Props) => { return } if (route?.params?.flowId) { - fetchFlow(sdk, sessionToken, route.params.flowId).then(setFlow) + fetchFlow(sdk, sessionToken, route.params.flowId) + .then(setFlow) + .catch(logSDKError) } else { - initializeFlow(sdk, sessionToken).then(setFlow) + initializeFlow(sdk, sessionToken).then(setFlow).catch(logSDKError) } }, [sdk, sessionToken]) @@ -70,16 +71,14 @@ const Settings = ({ navigation, route }: Props) => { const onSuccess = (result: SettingsFlow) => { if (result.continue_with) { - if (result.continue_with) { - for (const c of result.continue_with) { - switch (c.action) { - case "show_verification_ui": { - console.log("got a verification flow, navigating to it", c) - navigation.navigate("Verification", { - flowId: c.flow.id, - }) - break - } + for (const c of result.continue_with) { + switch (c.action) { + case "show_verification_ui": { + console.log("got a verification flow, navigating to it", c) + navigation.navigate("Verification", { + flowId: c.flow.id, + }) + break } } } diff --git a/src/components/Routes/Verification.tsx b/src/components/Routes/Verification.tsx index 4da0602..993162a 100644 --- a/src/components/Routes/Verification.tsx +++ b/src/components/Routes/Verification.tsx @@ -12,6 +12,7 @@ import AuthSubTitle from "../Styled/AuthSubTitle" import NavigationCard from "../Styled/NavigationCard" import StyledButton from "../Styled/StyledButton" import StyledCard from "../Styled/StyledCard" +import { logSDKError } from "../../helpers/axios" type Props = StackScreenProps @@ -26,7 +27,7 @@ export default function Verification({ navigation, route }: Props) { .then(({ data: flow }) => { setFlow(flow) }) - .catch(console.error) + .catch(logSDKError) const fetchFlow = (id: string) => sdk @@ -34,7 +35,7 @@ export default function Verification({ navigation, route }: Props) { .then(({ data }) => { setFlow(data) }) - .catch(console.error) + .catch(logSDKError) // When the component is mounted, we initialize a new verification flow // or use the id provided by the route params to fetch that flow: diff --git a/src/helpers/axios.tsx b/src/helpers/axios.tsx index 82935bd..d054fd8 100644 --- a/src/helpers/axios.tsx +++ b/src/helpers/axios.tsx @@ -1,6 +1,7 @@ // A small which adds retries to axios -import { AxiosInstance } from "axios" +import axios, { AxiosInstance } from "axios" +import { format } from "@redtea/format-axios-error" export const resilience = (axios: AxiosInstance) => { axios.interceptors.response.use( @@ -59,3 +60,50 @@ export const resilience = (axios: AxiosInstance) => { }, ) } + +function containsGenericError( + data: unknown, +): data is { error: { id?: string } & Record } { + if (!data) { + return false + } + if (typeof data !== "object") { + return false + } + + if (!("error" in data)) { + return false + } + + const e = data.error + return typeof e === "object" && !!e +} + +export function logSDKError(e: unknown): void { + if (!axios.isAxiosError(e)) { + console.error("Something went wrong", JSON.stringify(e, null, 2)) + return + } + const data = e.response?.data + + if (!containsGenericError(data)) { + console.error("Something went wrong", format(e)) + return + } + + let message = undefined + switch (data.error.id) { + case "self_service_flow_return_to_forbidden": + message = + "Your project does not allow to return to the app. Please add the URL to the allowed_return_to URLs." + break + } + + console.error( + message || data.error.reason || "Something went wrong", + "\n\n", + "error details:", + JSON.stringify(data.error, null, 2), + ) + return +} diff --git a/src/helpers/form.tsx b/src/helpers/form.tsx index f71da38..4daaffa 100644 --- a/src/helpers/form.tsx +++ b/src/helpers/form.tsx @@ -1,7 +1,9 @@ import { + ErrorBrowserLocationChangeRequired, FrontendApiExchangeSessionTokenRequest, GenericError, LoginFlow, + RecoveryFlow, RegistrationFlow, UiNode, UiNodeAnchorAttributes, @@ -11,11 +13,15 @@ import { UiNodeTextAttributes, VerificationFlow, } from "@ory/client" -import { showMessage } from "react-native-flash-message" +import { AxiosError } from "axios" +import * as AuthSession from "expo-auth-session" import * as WebBrowser from "expo-web-browser" +import { showMessage } from "react-native-flash-message" import { SessionContext } from "./auth" +import { logSDKError } from "./axios" import { newOrySdk } from "./sdk" -import { AxiosError } from "axios" + +type Flow = LoginFlow | RegistrationFlow | VerificationFlow | RecoveryFlow export function camelize(str: string) { return str.replace(/_([a-z])/g, (g) => g[1].toUpperCase()) as keyof T @@ -76,8 +82,21 @@ export const getNodeTitle = ({ attributes, meta }: UiNode): string => { return "" } -export function handleFlowInitError(err: AxiosError) { - return +function isErrorGeneric(data: unknown): data is { error: GenericError } { + return ( + typeof data === "object" && + data !== null && + "error" in data && + typeof data.error === "object" + ) +} + +function isRedirectBrowserToError( + data: unknown, +): data is ErrorBrowserLocationChangeRequired { + return ( + typeof data === "object" && data !== null && "redirect_browser_to" in data + ) } export function handleFormSubmitError< @@ -91,11 +110,12 @@ export function handleFormSubmitError< logout?: () => void, ) { return (err: AxiosError) => { + logSDKError(err) if (err.response) { switch (err.response.status) { case 400: - if (typeof err.response.data.error === "object") { - const ge: GenericError = err.response.data + if (isErrorGeneric(err.response.data)) { + const ge = err.response.data.error showMessage({ message: `${ge.message}: ${ge.reason}`, type: "danger", @@ -104,14 +124,17 @@ export function handleFormSubmitError< return Promise.resolve() } - console.debug("Form validation failed:", err.response.data) + console.debug( + "Form validation failed:", + JSON.stringify(err.response.data), + ) setFlow(err.response.data) return Promise.resolve() case 404: case 410: // This happens when the flow is, for example, expired or was deleted. // We simply re-initialize the flow if that happens! - console.debug("Flow could not be found, reloading page.") + console.debug("Flow could not be found, re-initializing the flow.") initializeFlow() return Promise.resolve() case 403: @@ -128,7 +151,7 @@ export function handleFormSubmitError< // This happens when the privileged session is expired but the user tried // to modify a privileged field (e.g. change the password). console.warn( - "The server indicated that this action is not allowed for you. The most likely cause of that is that you modified a privileged field (e.g. your password) but your ORY Kratos Login Session is too old.", + "The server indicated that this action is not allowed for you. The most likely cause of that is that you modified a privileged field (e.g. your password) but your Ory Identities Login Session is too old.", ) showMessage({ message: "Please re-authenticate before making these changes.", @@ -137,17 +160,25 @@ export function handleFormSubmitError< logout() return Promise.resolve() case 422: - handleRedirectBrowserTo( - err.response.data.redirect_browser_to, - flow, - setSession, - refetchFlow, + console.log( + "The server responded with a 422 error, which indicates that to complete this action, the user needs to fulfil additional steps.", + JSON.stringify(err.response.data, null, 2), ) + if ( + isRedirectBrowserToError(err.response.data) && + err.response.data.redirect_browser_to + ) { + handleRedirectBrowserTo( + err.response.data.redirect_browser_to, + flow as any, + setSession, + refetchFlow, + ) + } return Promise.resolve() } } - console.error(err, err.response?.data) return Promise.resolve() } } @@ -169,14 +200,28 @@ async function handleRedirectBrowserTo( const result = await WebBrowser.openAuthSessionAsync( url, - "http://localhost:19006/Callback", + AuthSession.makeRedirectUri({ + preferLocalhost: true, + path: "/Callback", + }), ) if (result.type == "success") { // We can fetch the session token now! const initCode = flow?.session_token_exchange_code + if (!initCode) { + console.log( + "The code from the flow is missing, refetching flow. This is likely due to an error in the flow.", + JSON.stringify(flow), + ) + return refetchFlow() + } const returnToCode = new URL(result.url).searchParams.get("code") - if (!initCode || !returnToCode) { - console.log("code missing, refetching flow") + if (!returnToCode) { + console.log( + "The provider did not include a code, refetching flow. This is likely due to an error in the flow.", + "The url was: ", + result.url, + ) return refetchFlow() } fetchToken({ initCode, returnToCode }) diff --git a/src/helpers/sdk.tsx b/src/helpers/sdk.tsx index ff62d58..49d94a3 100644 --- a/src/helpers/sdk.tsx +++ b/src/helpers/sdk.tsx @@ -1,9 +1,7 @@ import { Configuration, FrontendApi } from "@ory/client" -import axiosFactory from "axios" +import axios from "axios" import Constants from "expo-constants" -const axios = axiosFactory.create() - // canonicalize removes the trailing slash from URLs. const canonicalize = (url: string = "") => url.replace(/\/+$/, "") @@ -27,7 +25,7 @@ export const newOrySdk = (project: string) => basePath: kratosUrl(project), baseOptions: { // Setting this is very important as axios will send the CSRF cookie otherwise - // which causes problems with ORY Kratos' security detection. + // which causes problems with Ory Kratos' security detection. withCredentials: false, // Timeout after 5 seconds. diff --git a/src/translations.ts b/src/translations.ts deleted file mode 100644 index 874addf..0000000 --- a/src/translations.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -import { FormField } from "@ory/kratos-client" - -const translations = { - password: { - title: "Password", - position: 2, - }, - "traits.email": { - title: "E-Mail", - position: 1, - }, - "traits.name.first": { - title: "First Name", - position: 2, - }, - "traits.name.last": { - title: "Last Name", - position: 3, - }, - "traits.name.birthday": { - title: "My Birthday", - position: 4, - }, - "traits.website": { - title: "Website", - position: 4, - }, - identifier: { - title: "E-Mail", - position: 0, - }, - to_verify: { - title: "Your email address", - position: 0, - }, -} - -type Translations = typeof translations - -export const getTitle = (key: string): string => - key in translations ? translations[key as keyof Translations].title : key - -export const getPosition = (field: FormField) => - field.name && field.name in translations - ? translations[field.name as keyof Translations].position - : Infinity