diff --git a/SETUP.md b/SETUP.md index d72944a..3d03563 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,4 +1,8 @@ -# SurveyMonkey Instructions +# SurveyMonkey Setup Instructions + +Follow these steps to install and configure the SurveyMonkey app using either an Access Token or OAuth credentials. + +## Using Access Token First, go to the SurveyMonkey developer website ([developer.surveymonkey.com](https://developer.surveymonkey.com), or [developer.eu.surveymonkey.com](https://developer.eu.surveymonkey.com/) if you are based in EU) and log in to your account. @@ -17,3 +21,26 @@ While on the settings tab in the SurveyMonkey developer website, scroll down to [![](/docs/assets/setup/surveymonkey_setup_03.png)](/docs/assets/setup/surveymonkey_setup_03.png) Once you're happy with your settings, click the "Install" button to install the app. + +## Using OAuth + +First, go to the SurveyMonkey developer website ([developer.surveymonkey.com](https://developer.surveymonkey.com), or [developer.eu.surveymonkey.com](https://developer.eu.surveymonkey.com/) if you are based in EU) and log in to your account. + +[![](/docs/assets/setup/surveymonkey_setup_01.png)](/docs/assets/setup/surveymonkey_setup_01.png) + +Once you're logged in, click on the __"My Apps"__ tab at the top. After that click on __"Add a New App"__, and fill in the required fields, making sure to select Private App as the App Type. + +[![](/docs/assets/setup/surveymonkey_setup_02.png)](/docs/assets/setup/surveymonkey_setup_02.png) + + +On the "Details" page copy the `Client ID` and `Secret` and input the credentials in the settings tab in Deskpro. + +[![](/docs/assets/setup/surveymonkey_setup_04.png)](/docs/assets/setup/surveymonkey_setup_04.png) + +Head over to the __Settings__ tab in the SurveyMonkey website, and enter the `Callback URL` from Deskpro in the "OAuth Redirect URL" field. + +While on the settings tab in the SurveyMonkey developer website, scroll down to scopes and set __View Response Details__, __View Contacts__, __View Surveys__, __View Collectors__, __View Users__, and __View Responses__ to "Required", and click on Update Scopes. + +[![](/docs/assets/setup/surveymonkey_setup_03.png)](/docs/assets/setup/surveymonkey_setup_03.png) + +Once you're happy with your settings, click the "Install" button to install the app. diff --git a/docs/assets/setup/surveymonkey_setup_04.png b/docs/assets/setup/surveymonkey_setup_04.png new file mode 100644 index 0000000..0b79607 Binary files /dev/null and b/docs/assets/setup/surveymonkey_setup_04.png differ diff --git a/jest.config.js b/jest.config.js index 6b41972..75e56d2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -35,6 +35,7 @@ module.exports = { "^.+\\.mjs$": "@swc/jest", }, moduleNameMapper: { + "^@/(.*)$": "/src/$1", "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/config/jest/fileTransform.js", "\\.(css|less)$": "/config/jest/fileTransform.js", diff --git a/manifest.json b/manifest.json index f3453be..43c2648 100644 --- a/manifest.json +++ b/manifest.json @@ -8,31 +8,101 @@ "isSingleInstall": false, "hasDevMode": true, "serveUrl": "https://apps-cdn.deskpro-service.com/__name__/__version__", - "targets": [{ "target": "ticket_sidebar", "entrypoint": "index.html" }], + "secrets": "duigHU4j5LpEnl/z10+g9hlSrHOF6k2LIrpDyjiitWrP+9Axm1b7yofWi5PIAPzmUiwvP+SxlayYwmLGdc+Qq/qnsmzcZ4RVdd5gnqqhEv88w8skemjyFtk8/dMlrGfUY/prhhhm2Km3X5Y18+8cWVCCI0d36fKHuo33TgXdUg1QkezWeYflUj7PVCJgIARlXg6S/Rn65++O/6DMVXkxRP8r59Y0AhIUgJY9rxNRCneXUHG59cJ4Jx5OcHtbzIKNlpiWAfzsjzccNlveHSDGRzkiYraflh/YYMIWe1iheEa5+WvZpQidx4KAYqqrsYYkN1+1Nk5+Dh9C74pE+epoW5q+me4CcJR5CfAlMpMZnr32ecyJqkyWBmMk1swRnhlTNx7BEcmzy3xyAFscR/fhdJlQIkcbq9/YykRiIAKDAFmvLYhn4SbwVZMhlWTOvn2Ottt82HRr7lP6ZKZNkqaeuW63egzrowcJxR6KJRcDQqYrCQoLHOyK4wnqJ+XCU1DW4A2nAZJmTkykkgIwJt+eMKOmrErh9QArSibV+X6XBc14vUGRwed62GhzNoWZv/kU//gp8AZkwWRGhYbUQcxT+JPcN4HOqvJSPofJXTuDKZ6879oSO6S0mAuE87OzFWRKuGOhMgoXvwxzysXTnEDKsMOKZVsmlD3GWR/wReVk4q3OAGPpLo6z5f1Ly5PJtSA7KLhMUo37a+UEto6/H935oiSuHZQb1XYqvxT5Wm3k2EuFVMqqL9zxaUSKR6bbunX9mrL00lTRQTTNVjyj5bk6gcWc0oZFoZDlKbJCfS2al5pJMAsjZEFAdRtUi06WnHtKKpAYULbyBEFE+fSs9a7A9JuXyEXNYXha6c8biniO9yUr/XrKrOo0PAyaEFgRum7mHdPTlf3OJ5AQR1f+WgvpU9zm/tAou6gGJYtp0e/ePIBnaMT+cCTZPbxJcgR9iZ21RPwdiD5YTSehTuDjvpsEwKHJiGSW57BxLcJasUQ9CWjGQmBtm2YKtQ8g2/W03WuvOY8QwfZSpKyW3pHWtNSUVKjOjh4pldRXjwEjWSmRW/MH1zyQIBSGV9WF4pir9ny0tdE9cA/VQGzYZUYM7Rc+zw==", + "targets": [ + { + "target": "ticket_sidebar", + "entrypoint": "index.html" + } + ], "settings": { - "api_key": { - "title": "API Key", - "description": "The API key can be obtained by following our setup guide", + "use_advanced_connect": { + "title": "Advanced Connect", + "description": "Follow the setup guide and use your credentials to connect the app to Deskpro.", + "type": "boolean", + "default": false, + "isRequired": false, + "isBackendOnly": false, + "order": 5 + }, + "use_access_token": { + "title": "Use Access Token", + "type": "boolean", + "isRequired": false, + "isBackendOnly": false, + "default": false, + "condition": "settings.use_advanced_connect != false", + "order": 10 + }, + "access_token": { + "title": "Access Token", + "description": "The access token can be obtained by following our setup guide", "type": "string", - "isRequired": true, - "isBackendOnly": true + "isRequired": false, + "isBackendOnly": true, + "condition": "settings.use_advanced_connect != false && settings.use_access_token == true", + "order": 20 }, "verify_settings": { "title": "", "type": "app_embedded", - "options": { "entrypoint": "#/admin/verify_settings" }, + "options": { + "entrypoint": "#/admin/verify_settings", + "height": "30px" + }, "isRequired": false, "isBackendOnly": true, - "order": 20 + "condition": "settings.use_advanced_connect != false && settings.use_access_token == true", + "order": 30 + }, + "client_id": { + "title": "Client ID", + "type": "string", + "isRequired": false, + "isBackendOnly": false, + "condition": "settings.use_advanced_connect != false && settings.use_access_token != true", + "order": 40 + }, + "client_secret": { + "title": "Client Secret", + "type": "string", + "isRequired": false, + "isBackendOnly": true, + "condition": "settings.use_advanced_connect != false && settings.use_access_token != true", + "order": 50 + }, + "callback_url": { + "title": "Callback URL", + "type": "app_embedded", + "options": { + "entrypoint": "#/admin/callback" + }, + "isRequired": false, + "isBackendOnly": true, + "condition": "settings.use_advanced_connect != false && settings.use_access_token != true", + "order": 60 } }, "proxy": { "whitelist": [ { + "url": "https://api.surveymonkey.net/v3/.*", - "methods": ["GET", "POST", "PATCH"], + "methods": [ + "GET", + "POST", + "PATCH" + ], + "timeout": 20 + }, + { + + "url": "https://api.surveymonkey.com/oauth/.*", + "methods": [ + "POST" + ], "timeout": 20 } ] } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 82785de..ff2d942 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "bumpManifestVer": "node ./bin/bumpManifestVer.js" }, "dependencies": { - "@deskpro/app-sdk": "^5.1.1", + "@deskpro/app-sdk": "^6.0.3", "@deskpro/deskpro-ui": "^8.2.0", "@heroicons/react": "1.0.6", "@tanstack/react-query": "^4.36.1", @@ -53,6 +53,6 @@ "styled-components": "^6.1.14", "ts-jest": "^27.1.5", "typescript": "^5.7.2", - "vite": "^6.0.11" + "vite": "^6.2.2" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93b922b..3c31a84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@deskpro/app-sdk': - specifier: ^5.1.1 - version: 5.1.1(@deskpro/deskpro-ui@8.2.0(@types/web@0.0.86)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) + specifier: ^6.0.3 + version: 6.0.3(@deskpro/deskpro-ui@8.2.0(@types/web@0.0.86)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) '@deskpro/deskpro-ui': specifier: ^8.2.0 version: 8.2.0(@types/web@0.0.86)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) @@ -121,8 +121,8 @@ importers: specifier: ^5.7.2 version: 5.7.2 vite: - specifier: ^6.0.11 - version: 6.0.11(@types/node@22.10.5)(sass@1.83.1) + specifier: ^6.2.2 + version: 6.2.2(@types/node@22.10.5)(sass@1.84.0) packages: @@ -326,8 +326,8 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@deskpro/app-sdk@5.1.1': - resolution: {integrity: sha512-GVwjVb/8EX4aBHtDu7YqWKpVPxslyFy0nAEjbo59hycy73KPqV0UdOCQNM5LasZnrC1U6epV/BYVQrUDplTzuw==} + '@deskpro/app-sdk@6.0.3': + resolution: {integrity: sha512-7IYRxJ6SRCKrsSFO5bZwGYhRoEvX08LPv0pDGIxhNejrqp+eaDb8hjobKnPKwK22pRNJ/riExGPsbGXeSuuCZQ==} engines: {node: '>=20.0.0'} peerDependencies: '@deskpro/deskpro-ui': ^8.0.0 @@ -360,152 +360,152 @@ packages: '@emotion/unitless@0.8.1': resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} - '@esbuild/aix-ppc64@0.24.2': - resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + '@esbuild/aix-ppc64@0.25.1': + resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.24.2': - resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + '@esbuild/android-arm64@0.25.1': + resolution: {integrity: sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.24.2': - resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + '@esbuild/android-arm@0.25.1': + resolution: {integrity: sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.24.2': - resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + '@esbuild/android-x64@0.25.1': + resolution: {integrity: sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.24.2': - resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + '@esbuild/darwin-arm64@0.25.1': + resolution: {integrity: sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.24.2': - resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + '@esbuild/darwin-x64@0.25.1': + resolution: {integrity: sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.24.2': - resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + '@esbuild/freebsd-arm64@0.25.1': + resolution: {integrity: sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.24.2': - resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + '@esbuild/freebsd-x64@0.25.1': + resolution: {integrity: sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.24.2': - resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + '@esbuild/linux-arm64@0.25.1': + resolution: {integrity: sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.24.2': - resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + '@esbuild/linux-arm@0.25.1': + resolution: {integrity: sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.24.2': - resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + '@esbuild/linux-ia32@0.25.1': + resolution: {integrity: sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.24.2': - resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + '@esbuild/linux-loong64@0.25.1': + resolution: {integrity: sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.24.2': - resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + '@esbuild/linux-mips64el@0.25.1': + resolution: {integrity: sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.24.2': - resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + '@esbuild/linux-ppc64@0.25.1': + resolution: {integrity: sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.24.2': - resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + '@esbuild/linux-riscv64@0.25.1': + resolution: {integrity: sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.24.2': - resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + '@esbuild/linux-s390x@0.25.1': + resolution: {integrity: sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.24.2': - resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + '@esbuild/linux-x64@0.25.1': + resolution: {integrity: sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.24.2': - resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + '@esbuild/netbsd-arm64@0.25.1': + resolution: {integrity: sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.24.2': - resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + '@esbuild/netbsd-x64@0.25.1': + resolution: {integrity: sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.24.2': - resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + '@esbuild/openbsd-arm64@0.25.1': + resolution: {integrity: sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.24.2': - resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + '@esbuild/openbsd-x64@0.25.1': + resolution: {integrity: sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.24.2': - resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + '@esbuild/sunos-x64@0.25.1': + resolution: {integrity: sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.24.2': - resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + '@esbuild/win32-arm64@0.25.1': + resolution: {integrity: sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.24.2': - resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + '@esbuild/win32-ia32@0.25.1': + resolution: {integrity: sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.24.2': - resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + '@esbuild/win32-x64@0.25.1': + resolution: {integrity: sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1583,8 +1583,8 @@ packages: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} - esbuild@0.24.2: - resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + esbuild@0.25.1: + resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==} engines: {node: '>=18'} hasBin: true @@ -2278,8 +2278,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.11.17: - resolution: {integrity: sha512-Jr6v8thd5qRlOlc6CslSTzGzzQW03uiscab7KHQZX1Dfo4R6n6FDhZ0Hri6/X7edLIDv9gl4VMZXhxTjLnl0VQ==} + libphonenumber-js@1.11.19: + resolution: {integrity: sha512-bW/Yp/9dod6fmyR+XqSUL1N5JE7QRxQ3KrBIbYS1FTv32e5i3SEtQVX+71CYNv8maWNSOgnlCoNp9X78f/cKiA==} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2518,8 +2518,8 @@ packages: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -2727,8 +2727,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sass@1.83.1: - resolution: {integrity: sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==} + sass@1.84.0: + resolution: {integrity: sha512-XDAbhEPJRxi7H0SxrnOpiXFQoUJHwkR2u3Zc4el+fK/Tt5Hpzw5kkQ59qVDfvdaUq6gCrEZIbySFBM2T9DNKHg==} engines: {node: '>=14.0.0'} hasBin: true @@ -3035,8 +3035,8 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} - vite@6.0.11: - resolution: {integrity: sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==} + vite@6.2.2: + resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -3410,7 +3410,7 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@deskpro/app-sdk@5.1.1(@deskpro/deskpro-ui@8.2.0(@types/web@0.0.86)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)': + '@deskpro/app-sdk@6.0.3(@deskpro/deskpro-ui@8.2.0(@types/web@0.0.86)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)': dependencies: '@deskpro/deskpro-ui': 8.2.0(@types/web@0.0.86)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@fortawesome/fontawesome-svg-core': 6.7.2 @@ -3426,7 +3426,7 @@ snapshots: fuse.js: 7.0.0 handlebars: 4.7.7 i18n-iso-countries: 7.13.0 - libphonenumber-js: 1.11.17 + libphonenumber-js: 1.11.19 modern-normalize: 1.1.0 penpal: 6.2.1 react: 18.3.1 @@ -3435,7 +3435,7 @@ snapshots: react-flatpickr: 3.10.7(react@18.3.1) react-intl: 5.25.1(react@18.3.1)(typescript@5.7.2) regenerator-runtime: 0.14.1 - sass: 1.83.1 + sass: 1.84.0 shortcut-buttons-flatpickr: 0.4.0 styled-components: 6.1.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-pattern: 4.3.0 @@ -3490,79 +3490,79 @@ snapshots: '@emotion/unitless@0.8.1': {} - '@esbuild/aix-ppc64@0.24.2': + '@esbuild/aix-ppc64@0.25.1': optional: true - '@esbuild/android-arm64@0.24.2': + '@esbuild/android-arm64@0.25.1': optional: true - '@esbuild/android-arm@0.24.2': + '@esbuild/android-arm@0.25.1': optional: true - '@esbuild/android-x64@0.24.2': + '@esbuild/android-x64@0.25.1': optional: true - '@esbuild/darwin-arm64@0.24.2': + '@esbuild/darwin-arm64@0.25.1': optional: true - '@esbuild/darwin-x64@0.24.2': + '@esbuild/darwin-x64@0.25.1': optional: true - '@esbuild/freebsd-arm64@0.24.2': + '@esbuild/freebsd-arm64@0.25.1': optional: true - '@esbuild/freebsd-x64@0.24.2': + '@esbuild/freebsd-x64@0.25.1': optional: true - '@esbuild/linux-arm64@0.24.2': + '@esbuild/linux-arm64@0.25.1': optional: true - '@esbuild/linux-arm@0.24.2': + '@esbuild/linux-arm@0.25.1': optional: true - '@esbuild/linux-ia32@0.24.2': + '@esbuild/linux-ia32@0.25.1': optional: true - '@esbuild/linux-loong64@0.24.2': + '@esbuild/linux-loong64@0.25.1': optional: true - '@esbuild/linux-mips64el@0.24.2': + '@esbuild/linux-mips64el@0.25.1': optional: true - '@esbuild/linux-ppc64@0.24.2': + '@esbuild/linux-ppc64@0.25.1': optional: true - '@esbuild/linux-riscv64@0.24.2': + '@esbuild/linux-riscv64@0.25.1': optional: true - '@esbuild/linux-s390x@0.24.2': + '@esbuild/linux-s390x@0.25.1': optional: true - '@esbuild/linux-x64@0.24.2': + '@esbuild/linux-x64@0.25.1': optional: true - '@esbuild/netbsd-arm64@0.24.2': + '@esbuild/netbsd-arm64@0.25.1': optional: true - '@esbuild/netbsd-x64@0.24.2': + '@esbuild/netbsd-x64@0.25.1': optional: true - '@esbuild/openbsd-arm64@0.24.2': + '@esbuild/openbsd-arm64@0.25.1': optional: true - '@esbuild/openbsd-x64@0.24.2': + '@esbuild/openbsd-x64@0.25.1': optional: true - '@esbuild/sunos-x64@0.24.2': + '@esbuild/sunos-x64@0.25.1': optional: true - '@esbuild/win32-arm64@0.24.2': + '@esbuild/win32-arm64@0.25.1': optional: true - '@esbuild/win32-ia32@0.24.2': + '@esbuild/win32-ia32@0.25.1': optional: true - '@esbuild/win32-x64@0.24.2': + '@esbuild/win32-x64@0.25.1': optional: true '@eslint/eslintrc@1.4.1': @@ -4777,33 +4777,33 @@ snapshots: dependencies: es-errors: 1.3.0 - esbuild@0.24.2: + esbuild@0.25.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.24.2 - '@esbuild/android-arm': 0.24.2 - '@esbuild/android-arm64': 0.24.2 - '@esbuild/android-x64': 0.24.2 - '@esbuild/darwin-arm64': 0.24.2 - '@esbuild/darwin-x64': 0.24.2 - '@esbuild/freebsd-arm64': 0.24.2 - '@esbuild/freebsd-x64': 0.24.2 - '@esbuild/linux-arm': 0.24.2 - '@esbuild/linux-arm64': 0.24.2 - '@esbuild/linux-ia32': 0.24.2 - '@esbuild/linux-loong64': 0.24.2 - '@esbuild/linux-mips64el': 0.24.2 - '@esbuild/linux-ppc64': 0.24.2 - '@esbuild/linux-riscv64': 0.24.2 - '@esbuild/linux-s390x': 0.24.2 - '@esbuild/linux-x64': 0.24.2 - '@esbuild/netbsd-arm64': 0.24.2 - '@esbuild/netbsd-x64': 0.24.2 - '@esbuild/openbsd-arm64': 0.24.2 - '@esbuild/openbsd-x64': 0.24.2 - '@esbuild/sunos-x64': 0.24.2 - '@esbuild/win32-arm64': 0.24.2 - '@esbuild/win32-ia32': 0.24.2 - '@esbuild/win32-x64': 0.24.2 + '@esbuild/aix-ppc64': 0.25.1 + '@esbuild/android-arm': 0.25.1 + '@esbuild/android-arm64': 0.25.1 + '@esbuild/android-x64': 0.25.1 + '@esbuild/darwin-arm64': 0.25.1 + '@esbuild/darwin-x64': 0.25.1 + '@esbuild/freebsd-arm64': 0.25.1 + '@esbuild/freebsd-x64': 0.25.1 + '@esbuild/linux-arm': 0.25.1 + '@esbuild/linux-arm64': 0.25.1 + '@esbuild/linux-ia32': 0.25.1 + '@esbuild/linux-loong64': 0.25.1 + '@esbuild/linux-mips64el': 0.25.1 + '@esbuild/linux-ppc64': 0.25.1 + '@esbuild/linux-riscv64': 0.25.1 + '@esbuild/linux-s390x': 0.25.1 + '@esbuild/linux-x64': 0.25.1 + '@esbuild/netbsd-arm64': 0.25.1 + '@esbuild/netbsd-x64': 0.25.1 + '@esbuild/openbsd-arm64': 0.25.1 + '@esbuild/openbsd-x64': 0.25.1 + '@esbuild/sunos-x64': 0.25.1 + '@esbuild/win32-arm64': 0.25.1 + '@esbuild/win32-ia32': 0.25.1 + '@esbuild/win32-x64': 0.25.1 escalade@3.2.0: {} @@ -5762,7 +5762,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.11.17: {} + libphonenumber-js@1.11.19: {} lines-and-columns@1.2.4: {} @@ -5965,7 +5965,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.4.49: + postcss@8.5.3: dependencies: nanoid: 3.3.8 picocolors: 1.1.1 @@ -6191,7 +6191,7 @@ snapshots: safer-buffer@2.1.2: {} - sass@1.83.1: + sass@1.84.0: dependencies: chokidar: 4.0.3 immutable: 5.0.3 @@ -6487,15 +6487,15 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - vite@6.0.11(@types/node@22.10.5)(sass@1.83.1): + vite@6.2.2(@types/node@22.10.5)(sass@1.84.0): dependencies: - esbuild: 0.24.2 - postcss: 8.4.49 + esbuild: 0.25.1 + postcss: 8.5.3 rollup: 4.32.1 optionalDependencies: '@types/node': 22.10.5 fsevents: 2.3.3 - sass: 1.83.1 + sass: 1.84.0 w3c-xmlserializer@4.0.0: dependencies: diff --git a/src/App.tsx b/src/App.tsx index 47661d6..573c748 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,11 @@ -import { LoadingSpinner } from "@deskpro/app-sdk"; -import { Suspense } from "react"; +import { AdminCallbackPage, HomePage, LoadingPage, LoginPage, VerifySettingsPage } from "./pages"; import { ErrorBoundary } from "react-error-boundary"; +import { ErrorFallback } from "./components/ErrorFallback/ErrorFallback"; import { HashRouter, Route, Routes } from "react-router-dom"; -import { Main } from "./pages/Main"; -import { VerifySettings } from "./pages/VerifySettings"; +import { LoadingSpinner } from "@deskpro/app-sdk"; import { QueryErrorResetBoundary } from "@tanstack/react-query"; -import { ErrorFallback } from "./components/ErrorFallback/ErrorFallback"; -import { Redirect } from "./components/Redirect/Redirect"; +import { Suspense } from "react"; + function App() { return ( @@ -16,9 +15,11 @@ function App() { {({ reset }) => ( - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> )} diff --git a/src/api/SurveyMonkey/getAccessToken.ts b/src/api/SurveyMonkey/getAccessToken.ts new file mode 100644 index 0000000..7ebc9db --- /dev/null +++ b/src/api/SurveyMonkey/getAccessToken.ts @@ -0,0 +1,32 @@ +import { IDeskproClient, proxyFetch } from "@deskpro/app-sdk"; + +export default async function getAccessToken( + client: IDeskproClient, + code: string, + callbackURL: string +) { + try { + const fetch = await proxyFetch(client); + + const response = await fetch(`https://api.surveymonkey.com/oauth/token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: "__client_id__", + client_secret: "__client_secret__", + code: code, + redirect_uri: callbackURL, + }).toString() + }); + + if (!response.ok) { + throw new Error("Failed to fetch access token"); + } + + const data = await response.json(); + return data; + } catch (error) { + throw new Error("Error fetching access token"); + } +} \ No newline at end of file diff --git a/src/api/SurveyMonkey/getCurrentUser.ts b/src/api/SurveyMonkey/getCurrentUser.ts new file mode 100644 index 0000000..242eb54 --- /dev/null +++ b/src/api/SurveyMonkey/getCurrentUser.ts @@ -0,0 +1,33 @@ +import { ACCESS_TOKEN_PATH, OAUTH2_ACCESS_TOKEN_PATH } from "@/constants/deskpro"; +import { adminGenericProxyFetch, IDeskproClient, proxyFetch as deskproProxyFetch } from "@deskpro/app-sdk"; +import { isResponseError } from "../api"; +import { User } from "@/types/user"; + +/** + * Returns the data of the active SurveyMonkey user + * + * @param client The Deskpro client + * @param accessToken Optional access token used for authentication (SHOULD ONLY BE USED IN THE ADMIN DRAWER) + */ +export default async function getCurrentUser(client: IDeskproClient, accessToken?: string): Promise { + try { + const proxyFetch = await (accessToken ? adminGenericProxyFetch : deskproProxyFetch)(client) + const isUsingOAuth2 = (await client.getUserState("isUsingOAuth"))[0]?.data + + const response = await proxyFetch(`https://api.surveymonkey.net/v3/users/me`, { + headers: { + method: "GET", + "Content-Type": "application/json", + // Set Authorization header based on the available credentials (Access token or OAuth2 token) + Authorization: ` Bearer ${accessToken ?? (isUsingOAuth2 ? `[user[${OAUTH2_ACCESS_TOKEN_PATH}]]` : ACCESS_TOKEN_PATH)}`, + }, + }); + + if (isResponseError(response)) { + throw new Error(await response.text()); + } + return await response.json(); + } catch { + return null + } +} diff --git a/src/api/SurveyMonkey/index.ts b/src/api/SurveyMonkey/index.ts new file mode 100644 index 0000000..600fc7e --- /dev/null +++ b/src/api/SurveyMonkey/index.ts @@ -0,0 +1,2 @@ +export {default as getCurrentUser} from "./getCurrentUser" +export {default as getAccessToken} from "./getAccessToken" \ No newline at end of file diff --git a/src/api/api.ts b/src/api/api.ts index 8225868..5d89876 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,33 +1,10 @@ -import { - proxyFetch, - IDeskproClient, - adminGenericProxyFetch, -} from "@deskpro/app-sdk"; -import { ISurvey, ISurveyWithDetails } from "../types/survey"; -import { ICollector, ICollectorWithDetails } from "../types/collector"; +import { ACCESS_TOKEN_PATH, OAUTH2_ACCESS_TOKEN_PATH } from "@/constants/deskpro"; +import { ICollector, ICollectorWithDetails } from "@/types/collector"; +import { ISurvey, ISurveyWithDetails } from "@/types/survey"; +import { proxyFetch, IDeskproClient } from "@deskpro/app-sdk"; export type RequestMethod = "GET" | "POST" | "PATCH" | "DELETE"; -export const getCurrentUser = async ( - client: IDeskproClient, - apiKey: string, -) => { - const dpFetch = await adminGenericProxyFetch(client); - const response = await dpFetch(`https://api.surveymonkey.net/v3/users/me`, { - headers: { - method: "GET", - "Content-Type": "application/json", - Authorization: ` Bearer ${apiKey}`, - }, - }); - - if (isResponseError(response)) { - throw new Error(await response.text()); - } - - return response.json(); -}; - export const getSurveysWithCollectors = async (client: IDeskproClient) => { const surveys = await getSurveys(client); @@ -125,11 +102,13 @@ export const request = async ( ) => { const fetch = await proxyFetch(client); + const isUsingOAuth2 = (await client.getUserState("isUsingOAuth"))[0]?.data + const options: RequestInit = { method, headers: { "Content-Type": "application/json", - Authorization: ` Bearer __api_key__`, + Authorization: ` Bearer ${isUsingOAuth2? `[user[${OAUTH2_ACCESS_TOKEN_PATH}]]` : ACCESS_TOKEN_PATH}`, }, }; diff --git a/src/components/Redirect/Redirect.tsx b/src/components/Redirect/Redirect.tsx deleted file mode 100644 index cdbc94d..0000000 --- a/src/components/Redirect/Redirect.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useEffect } from "react"; -import { useNavigate } from "react-router-dom"; - -export const Redirect = () => { - const navigate = useNavigate(); - useEffect(() => { - navigate("/"); - }, [navigate]); - - return
; -}; diff --git a/src/constants/deskpro.ts b/src/constants/deskpro.ts new file mode 100644 index 0000000..229c578 --- /dev/null +++ b/src/constants/deskpro.ts @@ -0,0 +1,4 @@ +export const OAUTH2_ACCESS_TOKEN_PATH = "oauth2/access_token" +export const OAUTH2_REFRESH_TOKEN_PATH = "oauth2/refresh_token" + +export const ACCESS_TOKEN_PATH = "__access_token__" diff --git a/src/hooks/deskpro/index.ts b/src/hooks/deskpro/index.ts new file mode 100644 index 0000000..6d8c1d9 --- /dev/null +++ b/src/hooks/deskpro/index.ts @@ -0,0 +1 @@ +export { useLogout } from "./useLogout" \ No newline at end of file diff --git a/src/hooks/deskpro/useLogout.ts b/src/hooks/deskpro/useLogout.ts new file mode 100644 index 0000000..eb71353 --- /dev/null +++ b/src/hooks/deskpro/useLogout.ts @@ -0,0 +1,25 @@ +import { OAUTH2_ACCESS_TOKEN_PATH } from "@/constants/deskpro"; +import { useCallback } from "react"; +import { useDeskproAppClient } from "@deskpro/app-sdk"; +import { useNavigate } from "react-router-dom"; + +export function useLogout() { + const navigate = useNavigate(); + const { client } = useDeskproAppClient(); + + const logoutActiveUser = useCallback(() => { + if (!client) { + return; + } + + client.setBadgeCount(0) + + client.deleteUserState(OAUTH2_ACCESS_TOKEN_PATH) + .catch(() => { }) + .finally(() => { + navigate("/login"); + }); + }, [client, navigate]); + + return { logoutActiveUser }; +} \ No newline at end of file diff --git a/src/pages/AdminCallbackPage.tsx b/src/pages/AdminCallbackPage.tsx new file mode 100644 index 0000000..e9f8172 --- /dev/null +++ b/src/pages/AdminCallbackPage.tsx @@ -0,0 +1,51 @@ +import { CopyToClipboardInput, LoadingSpinner, OAuth2Result, useInitialisedDeskproAppClient, } from "@deskpro/app-sdk"; +import { createSearchParams } from "react-router-dom"; +import { P1 } from "@deskpro/deskpro-ui"; +import { useState } from "react"; +import styled from "styled-components"; +import type { FC } from "react"; + +const Description = styled(P1)` + margin-top: 8px; + margin-bottom: 16px; + color: ${({ theme }) => theme.colors.grey80}; +`; + +const AdminCallbackPage: FC = () => { + const [callbackUrl, setCallbackUrl] = useState(null) + + useInitialisedDeskproAppClient(async (client) => { + const oauth2 = await client.startOauth2Local( + ({ callbackUrl, state }) => { + return `https://api.surveymonkey.com/oauth/authorize?${createSearchParams([ + ["response_type", "code"], + ["client_id", "xxx"], + ["state", state], + ["redirect_uri", callbackUrl], + ])}` + }, + /code=(?[0-9a-f]+)/, + async (): Promise => ({ data: { access_token: "", refresh_token: "" } }) + ) + + const url = new URL(oauth2.authorizationUrl); + const redirectUri = url.searchParams.get("redirect_uri") + + if (redirectUri) { + setCallbackUrl(redirectUri) + } + }) + + if (!callbackUrl) { + return () + } + + return ( + <> + + The callback URL will be required during the SurveyMonkey app setup + + ); +}; + +export default AdminCallbackPage diff --git a/src/pages/Loading/LoadingPage.tsx b/src/pages/Loading/LoadingPage.tsx new file mode 100644 index 0000000..2d6b539 --- /dev/null +++ b/src/pages/Loading/LoadingPage.tsx @@ -0,0 +1,80 @@ +import { ErrorBlock } from "@/components/ErrorBlock"; +import { FC, useState } from "react"; +import { getCurrentUser } from "@/api/SurveyMonkey"; +import { LoadingSpinner, useDeskproAppClient, useDeskproElements, useDeskproLatestAppContext, useInitialisedDeskproAppClient } from "@deskpro/app-sdk"; +import { Settings, ContextData } from "@/types/deskpro"; +import { Stack } from "@deskpro/deskpro-ui"; +import { useNavigate } from "react-router-dom"; + +const LoadingPage: FC = () => { + const { client } = useDeskproAppClient() + const { context } = useDeskproLatestAppContext() + + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isFetchingAuth, setIsFetchingAuth] = useState(true) + + const navigate = useNavigate(); + + // Determine authentication method from settings + const isUsingOAuth = context?.settings.use_access_token !== true || context.settings.use_advanced_connect === false + const user = context?.data?.ticket?.primaryUser || context?.data?.user + + useDeskproElements(({ registerElement, clearElements }) => { + clearElements() + registerElement("refreshButton", { type: "refresh_button" }) + }); + + useInitialisedDeskproAppClient((client) => { + client.setTitle("SurveyMonkey") + + if (!context || !context?.settings || !user) { + return + } + + // Store the authentication method in the user state + client.setUserState("isUsingOAuth", isUsingOAuth) + + // Verify authentication status + // If OAuth2 mode and the user is logged in the request would be make with their stored access token + // If access token mode the request would be made with the access token provided in the app setup + getCurrentUser(client) + .then((user) => { + if (user) { + setIsAuthenticated(true) + } + }) + .catch(() => { }) + .finally(() => { + setIsFetchingAuth(false) + }) + }, [context, context?.settings]) + + if (!client || !user || isFetchingAuth) { + return () + } + if (isAuthenticated) { + + navigate("/home") + } else { + + if (isUsingOAuth) { + navigate("/login") + } else { + // Show error for invalid access tokens (expired or not present) + return ( + + + + ) + } + + } + + + + return ( + + ); +}; + +export default LoadingPage; diff --git a/src/pages/Loading/index.ts b/src/pages/Loading/index.ts new file mode 100644 index 0000000..a583868 --- /dev/null +++ b/src/pages/Loading/index.ts @@ -0,0 +1 @@ +export { default } from "./LoadingPage"; diff --git a/src/pages/Login/LoginPage.tsx b/src/pages/Login/LoginPage.tsx new file mode 100644 index 0000000..3358df1 --- /dev/null +++ b/src/pages/Login/LoginPage.tsx @@ -0,0 +1,36 @@ +import { AnchorButton, H3, Stack } from "@deskpro/deskpro-ui" +import { ErrorBlock } from "@/components/ErrorBlock" +import { FC } from "react" +import { useDeskproElements, useInitialisedDeskproAppClient } from "@deskpro/app-sdk" +import useLogin from "./useLogin" + +const LoginPage: FC = () => { + useDeskproElements(({ registerElement, clearElements }) => { + clearElements() + registerElement("refreshButton", { type: "refresh_button" }) + }) + + useInitialisedDeskproAppClient((client)=>{ + client.setTitle("Login") + }, []) + + const { onSignIn, authUrl, isLoading, error } = useLogin(); + + return ( + +

Log into your SurveyMonkey account.

+ + + {error && } +
+ ) +} + +export default LoginPage \ No newline at end of file diff --git a/src/pages/Login/index.ts b/src/pages/Login/index.ts new file mode 100644 index 0000000..f815230 --- /dev/null +++ b/src/pages/Login/index.ts @@ -0,0 +1 @@ +export { default } from "./LoginPage"; diff --git a/src/pages/Login/useLogin.ts b/src/pages/Login/useLogin.ts new file mode 100644 index 0000000..0227b5c --- /dev/null +++ b/src/pages/Login/useLogin.ts @@ -0,0 +1,125 @@ +import { ContextData, Settings } from "@/types/deskpro"; +import { createSearchParams, useNavigate } from "react-router-dom"; +import { getAccessToken, getCurrentUser } from "@/api/SurveyMonkey"; +import { OAUTH2_ACCESS_TOKEN_PATH, OAUTH2_REFRESH_TOKEN_PATH } from "@/constants/deskpro"; +import { IOAuth2, OAuth2Result, useDeskproLatestAppContext, useInitialisedDeskproAppClient } from "@deskpro/app-sdk"; +import { useCallback, useState } from "react"; + +interface UseLogin { + onSignIn: () => void, + authUrl: string | null, + error: null | string, + isLoading: boolean, +}; + +export default function useLogin(): UseLogin { + const [authUrl, setAuthUrl] = useState(null) + const [error, setError] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [isPolling, setIsPolling] = useState(false) + const [oauth2Context, setOAuth2Context] = useState(null) + + const navigate = useNavigate() + + const { context } = useDeskproLatestAppContext() + + const user = context?.data?.ticket?.primaryUser || context?.data?.user + const isUsingOAuth = context?.settings.use_access_token !== true || context.settings.use_advanced_connect === false + + useInitialisedDeskproAppClient(async (client) => { + if (!user) { + return + } + + // Ensure they aren't using access tokens + if (!isUsingOAuth) { + setError("Enable OAuth to access this page"); + return + } + const mode = context?.settings.use_advanced_connect === false ? 'global' : 'local'; + + const clientId = context?.settings.client_id; + if (mode === 'local' && (typeof clientId !== 'string' || clientId.trim() === "")) { + // Local mode requires a clientId. + setError("A client ID is required"); + return + } + + // Start OAuth process depending on the authentication mode + const oauth2Response = mode === "local" ? + await client.startOauth2Local( + ({ state, callbackUrl }) => { + return `https://api.surveymonkey.com/oauth/authorize?${createSearchParams([ + ["client_id", clientId ?? ""], + ["state", state], + ["redirect_uri", callbackUrl], + ["response_type", "code"] + ])}`; + }, + /\bcode=(?[^&#]+)/, + async (code: string): Promise => { + // Extract the callback URL from the authorization URL + const url = new URL(oauth2Response.authorizationUrl); + const redirectUri = url.searchParams.get("redirect_uri"); + + if (!redirectUri) { + throw new Error("Failed to get callback URL"); + } + + const data = await getAccessToken(client, code, redirectUri); + + return { data } + } + ) + // Global Proxy Service + : await client.startOauth2Global("TW2mwcHyQwCmkrzNjgdMAQ"); + + setAuthUrl(oauth2Response.authorizationUrl) + setOAuth2Context(oauth2Response) + + }, [setAuthUrl, context?.settings.use_advanced_connect]) + + useInitialisedDeskproAppClient((client) => { + if (!user || !oauth2Context) { + return + } + + const startPolling = async () => { + try { + const result = await oauth2Context.poll() + + await client.setUserState(OAUTH2_ACCESS_TOKEN_PATH, result.data.access_token, { backend: true }) + + if (result.data.refresh_token) { + await client.setUserState(OAUTH2_REFRESH_TOKEN_PATH, result.data.refresh_token, { backend: true }) + } + + const activeUser = await getCurrentUser(client) + + if (!activeUser) { + throw new Error("Error authenticating user") + } + + navigate("/home") + } catch (error) { + setError(error instanceof Error ? error.message : 'Unknown error'); + } finally { + setIsLoading(false) + setIsPolling(false) + } + } + + if (isPolling) { + void startPolling() + } + }, [isPolling, user, oauth2Context, navigate]) + + const onSignIn = useCallback(() => { + setIsLoading(true); + setIsPolling(true); + window.open(authUrl ?? "", '_blank'); + }, [setIsLoading, authUrl]); + + return { authUrl, onSignIn, error, isLoading } + +} \ No newline at end of file diff --git a/src/pages/Main.tsx b/src/pages/Main.tsx index 7ba7fa6..20c23f1 100644 --- a/src/pages/Main.tsx +++ b/src/pages/Main.tsx @@ -1,24 +1,43 @@ -import { - LoadingSpinner, - useInitialisedDeskproAppClient, - useQueryWithClient, -} from "@deskpro/app-sdk"; -import { getSurveysWithCollectors } from "../api/api"; -import { FieldMapping } from "../components/FieldMapping/FieldMapping"; -import { Container } from "../components/Layout"; -import surveyJson from "../mapping/survey.json"; - -export const Main = () => { +import { AppElementPayload, LoadingSpinner, useDeskproAppEvents, useDeskproLatestAppContext, useInitialisedDeskproAppClient, useQueryWithClient, } from "@deskpro/app-sdk"; +import { Container } from "@/components/Layout"; +import { ContextData, Settings } from "@/types/deskpro"; +import { FieldMapping } from "@/components/FieldMapping/FieldMapping"; +import { getSurveysWithCollectors } from "@/api/api"; +import { useLogout } from "@/hooks/deskpro"; +import surveyJson from "@/mapping/survey.json"; + +const Main = () => { + const { logoutActiveUser } = useLogout() + const { context } = useDeskproLatestAppContext() + + const isUsingOAuth = context?.settings?.use_access_token !== true || context.settings.use_advanced_connect === false + useInitialisedDeskproAppClient((client) => { client.setTitle("SurveyMonkey"); client.registerElement("refreshButton", { type: "refresh_button" }); - client.registerElement("nutshellHomeButton", { + client.registerElement("homeButton", { type: "home_button", }); + if (isUsingOAuth) { + client.registerElement("menuButton", { type: "menu", items: [{ title: "Logout" }] }); + } }); + useDeskproAppEvents({ + onElementEvent(id: string, _type: string, _payload?: AppElementPayload) { + switch (id) { + case "menuButton": + if (isUsingOAuth) { + logoutActiveUser() + } + + break; + } + }, + }) + const surveysWithCollectorsQuery = useQueryWithClient(["abc"], (client) => getSurveysWithCollectors(client) ); @@ -43,3 +62,5 @@ export const Main = () => { ); }; + +export default Main diff --git a/src/pages/VerifySettings.tsx b/src/pages/VerifySettings.tsx index a60a143..136ca9e 100644 --- a/src/pages/VerifySettings.tsx +++ b/src/pages/VerifySettings.tsx @@ -1,12 +1,11 @@ -import { useState, useCallback } from "react"; -import styled from "styled-components"; +import { getCurrentUser } from "@/api/SurveyMonkey"; +import { nbsp, AUTH_ERROR } from "@/constants"; import { TSpan, Stack, Button } from "@deskpro/deskpro-ui"; import { useDeskproAppEvents, useDeskproAppClient } from "@deskpro/app-sdk"; -import { getCurrentUser } from "../api/api"; -import { nbsp, AUTH_ERROR } from "../constants"; +import { useState, useCallback } from "react"; +import styled from "styled-components"; import type { FC } from "react"; -import type { Settings } from "../types/settings"; -import type { User } from "../types/user"; +import type { User } from "@/types/user"; const Invalid = styled(TSpan)` color: ${({ theme }) => theme.colors.red100}; @@ -18,13 +17,13 @@ const Secondary = styled(TSpan)` const VerifySettings: FC = () => { const { client } = useDeskproAppClient(); - const [user, setUser] = useState(null); - const [settings, setSettings] = useState({}); + const [user, setUser] = useState(null); + const [settings, setSettings] = useState<{ access_token?: string }>({}); const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const onVerifySettings = useCallback(() => { - if (!client || !settings?.api_key) { + if (!client || !settings?.access_token) { return; } @@ -32,15 +31,18 @@ const VerifySettings: FC = () => { setError(""); setUser(null); - return getCurrentUser(client, settings.api_key) - .then(setUser) - .catch((err) => { - try { - setError(JSON.parse(err?.message || "{}")?.error?.message); - } catch (e) { - setError(AUTH_ERROR); + return getCurrentUser(client, settings.access_token) + .then((user)=>{ + if(user){ + setUser(user) + }else{ + setError(AUTH_ERROR) } }) + .catch(() => { + setError(AUTH_ERROR); + + }) .finally(() => setIsLoading(false)); }, [client, settings]); @@ -55,7 +57,7 @@ const VerifySettings: FC = () => { intent="secondary" onClick={onVerifySettings} loading={isLoading} - disabled={!settings?.api_key || isLoading} + disabled={!settings?.access_token || isLoading} /> {nbsp} {!user @@ -70,4 +72,4 @@ const VerifySettings: FC = () => { ); }; -export { VerifySettings }; +export default VerifySettings diff --git a/src/pages/index.ts b/src/pages/index.ts new file mode 100644 index 0000000..68e7159 --- /dev/null +++ b/src/pages/index.ts @@ -0,0 +1,5 @@ +export { default as AdminCallbackPage } from "./AdminCallbackPage" +export { default as VerifySettingsPage } from "./VerifySettings" +export { default as LoadingPage } from "./Loading" +export { default as LoginPage } from "./Login" +export { default as HomePage } from "./Main" \ No newline at end of file diff --git a/src/types/deskpro.ts b/src/types/deskpro.ts new file mode 100644 index 0000000..a772803 --- /dev/null +++ b/src/types/deskpro.ts @@ -0,0 +1,31 @@ +/** Deskpro types */ +export type Settings = { + client_id?: string, + use_advanced_connect?: boolean, + use_access_token?: boolean, + }; + + export type ContextData = { + ticket?: { + id: string, + subject: string, + permalinkUrl: string, + primaryUser: { + id: string, + email: string + displayName: string + firstName: string + lastName: string + } + }, + user?: { + id: string + isAgent: boolean + firstName: string + lastName: string + name: string + titlePrefix: string + primaryEmail: string + emails: string[] + } + }; \ No newline at end of file diff --git a/src/types/settings.ts b/src/types/settings.ts deleted file mode 100644 index adb69ca..0000000 --- a/src/types/settings.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Settings { - api_key?: string; -} diff --git a/src/types/surveymonkey.ts b/src/types/surveymonkey.ts new file mode 100644 index 0000000..6fe6af1 --- /dev/null +++ b/src/types/surveymonkey.ts @@ -0,0 +1,12 @@ +export type SurveyMonkeyScope = + | "users_read" + | "surveys_read" + | "collectors_read" + | "collectors_write" + | "contacts_read" + | "contacts_write" + | "surveys_write" + | "responses_read" + | "webhooks_read" + | "webhooks_write" + | "library_read" \ No newline at end of file diff --git a/tests/pages/Main.test.tsx b/tests/pages/Main.test.tsx index 2d8343d..495eefb 100644 --- a/tests/pages/Main.test.tsx +++ b/tests/pages/Main.test.tsx @@ -1,7 +1,7 @@ import { lightTheme, ThemeProvider } from "@deskpro/deskpro-ui"; import { cleanup, render, waitFor } from "@testing-library/react/"; import React from "react"; -import { Main } from "../../src/pages/Main"; +import Main from "../../src/pages/Main"; const renderPage = () => { return render( diff --git a/tsconfig.json b/tsconfig.json index 87248a1..492399f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,11 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "types": ["vite/client", "jest", "@testing-library/jest-dom", "@typescript/lib-dom"] + "types": ["vite/client", "jest", "@testing-library/jest-dom", "@typescript/lib-dom"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } }, "include": ["./src"] } diff --git a/vite.config.ts b/vite.config.ts index 345ace9..4f51e72 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,14 +1,22 @@ import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; import copy from "rollup-plugin-copy"; +import path from "path"; +import react from "@vitejs/plugin-react"; + // https://vitejs.dev/config/ export default defineConfig({ base: "", plugins: [react()], server:{ + port: 3003, allowedHosts: true }, + resolve:{ + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, build: { rollupOptions: { onwarn(warning, warn) {