diff --git a/SETUP.md b/SETUP.md index fffbdcc..6975699 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,8 +1,11 @@ Shopify App Setup === -To install the Shopify app you'll first need to create an API access token. Head over to your Shopify store and login -to the store admin. +Follow these steps to install and configure the Shopify app using either an Access Token or OAuth credentials. + +## Using Access Token + +Head over to your Shopify store and login to the store admin. From the store admin dashboard, navigate to the "Settings" page using the button in the bottom left. @@ -80,3 +83,39 @@ To configure who can see and use the Shopify app, head to the "Permissions" tab you'd like to have access. When you're happy, click "Install". + +## Using OAuth + +Go to your Shopify Partner account and navigate to the Apps section in the sidebar. On the "Apps" page, click "Create app". + +[![](/docs/assets/setup/shopify-setup-oauth-01.png)](/docs/assets/setup/shopify-setup-oauth-01.png) + +On the "Create a new app" page click "Create app manually" under "Use Shopify Partners", give your app a name (e.g. Deskpro) and click "Create". + +[![](/docs/assets/setup/shopify-setup-oauth-02.png)](/docs/assets/setup/shopify-setup-oauth-02.png) + +Once the app is created, you'll see a screen with its details. Copy the `Client ID` and `Client Secret`, then enter them in the Settings tab in Deskpro. + +[![](/docs/assets/setup/shopify-setup-oauth-05.png)](/docs/assets/setup/shopify-setup-oauth-05.png) + +Next, click "Choose distribution" on the same page to make your app live. + +Select a distribution method. Public apps require approval from Shopify, while Custom apps are intended for use on a single store without requiring approval. This guide follows the Custom app setup. + +[![](/docs/assets/setup/shopify-setup-oauth-03.png)](/docs/assets/setup/shopify-setup-oauth-03.png) + +Enter your store url and click "Generate Link" + + +Now, click Configuration in the sidebar. Enter the callback URL from the Settings tab in Deskpro into the "Allowed redirection URL(s)" field. Set "Embed app in Shopify admin" to `false`. + +[![](/docs/assets/setup/shopify-setup-oauth-04.png)](/docs/assets/setup/shopify-setup-oauth-04.png) + + +Once you've made these changes, click "Save and release". + +To configure who can see and use the Shopify app, head to the "Permissions" tab and select those users and/or groups +you'd like to have access. + +When you're happy, click "Install". + diff --git a/docs/assets/setup/shopify-setup-oauth-01.png b/docs/assets/setup/shopify-setup-oauth-01.png new file mode 100644 index 0000000..4dddda3 Binary files /dev/null and b/docs/assets/setup/shopify-setup-oauth-01.png differ diff --git a/docs/assets/setup/shopify-setup-oauth-02.png b/docs/assets/setup/shopify-setup-oauth-02.png new file mode 100644 index 0000000..53584e1 Binary files /dev/null and b/docs/assets/setup/shopify-setup-oauth-02.png differ diff --git a/docs/assets/setup/shopify-setup-oauth-03.png b/docs/assets/setup/shopify-setup-oauth-03.png new file mode 100644 index 0000000..f6d528c Binary files /dev/null and b/docs/assets/setup/shopify-setup-oauth-03.png differ diff --git a/docs/assets/setup/shopify-setup-oauth-04.png b/docs/assets/setup/shopify-setup-oauth-04.png new file mode 100644 index 0000000..efc3e6e Binary files /dev/null and b/docs/assets/setup/shopify-setup-oauth-04.png differ diff --git a/docs/assets/setup/shopify-setup-oauth-05.png b/docs/assets/setup/shopify-setup-oauth-05.png new file mode 100644 index 0000000..0004db9 Binary files /dev/null and b/docs/assets/setup/shopify-setup-oauth-05.png differ diff --git a/manifest.json b/manifest.json index c4aca8f..b0b2d30 100644 --- a/manifest.json +++ b/manifest.json @@ -8,9 +8,16 @@ "isSingleInstall": false, "hasDevMode": true, "serveUrl": "https://apps-cdn.deskpro-service.com/__name__/__version__", + "secrets": "hy5diyldVvLk+hwqrKPn3hy82isOK9JpbvD18IfoofGwpo1nNTup39yNWkuVIPODUrTIfuoY4nAwi+/QcTVK/7uecibF8+FY5Ibypyr/dwwDldkPB5CjyxPpW54fdUhjGfDRiwKvvvphqE+zpi57x49wPrANgZblWoaDvmYhvlObKIW7mTRO280EOzPactM4CHonN70pBydYkC2CAT3GeSGplmCTbS9vo1Hfgmz4SL2KsepeePVXnHaa0XIMreiUvCx05l7ozJvfZlxwClocy6k5WX2HHGF6PNxo/haXXlLCYpHiARIQ7F7+0UhSC9jqAxS77+LCORJXgM89+hew/pNQktTEQMLrr9i8Gy2s7G9DRSH96npgu+EeIkvHk1vn1hxJUwZgi+WLNn1lWGhmtXs+eLIFII/PMelE7EdphsTrwhf06JvMrNGys7ZImnA46RaacLa43I8JGyktD9Ll2l7Fvz5DoG7SIGO25DfA5pDivHa7gax/7ymm+5N5ltBTPUpBTi0Sigq1WCUBR9/bv+Jdp5QmbkBBr9EQnDARZrHo3DcClPC+CTQ4qofXql8B/5GwxqXuqVwoPo2DUN7lJWUx6p3TCYLZY7lvvHFm8HMsq/WJdMFasHa5+MphTBxtSVF4+vpSILBjQZj+7B2mEc+rqNYikufZHIP0aYYfmETByvD8sAxHz5XgGKA72HBfSHrPSQyNRUeZFcfq05QJx8fpa/ZzbhByiazQB3AmQ1rKhkq26SPmxojKmDcLBYaIwMmaplZEc5qTszIcRTZxOUn2WMlsovjOjW5C2LlFhYtqCoOhQR9Wu7bLoH85J7yleEXNQLfR28C3S0xPCbbdp0DRRdPOx8jT0qTXHrDuJJIDlkWf9iJJnE9ygTdWVczVm8PVK3duIqgzsfD5SrJyyfIxwQSv+wd9cNX9PbQ345iT2SGPvWRomBStTn9q8azA1ejsnVG7kIrNujSKIrp+9bSzGkkWcrFragaw2loyoyQs8OGrDVEbXdVdwyHxK0GlP7Gi17hplNx6a/ZrEyhsrdcqJBK+rR7uYM3mKcQniZJAUc+4fhANjGqUEYxNJ2ZKq79stOxU+QV+tXfB/36HaVJOIfLy2Z2K85c+0URADXzFS8mH9hjyLIUZuviMJHQtpngI6zwnZoXfK0eMxrOU+kC0+cQKD4r1niQGc1rYg6mFNKlq4E+0esEUgxJItKbMxPhQ4D+ZcrliokUfAlr0k6qThzWg3MT26mUlpn8Z1liwg3ojogBMogYsmfqB74JCulEgdN2EmpdsAU9lm9cmvJWV8eaHG2mLFxiux2RLQ+JYFmVjXCKW1y+xgfINublCW53QG28S/DmnKGI5DZim5+jqEkv5kTUXdL7XtCkxjm4heGWGhtX2/Or6XLBDOtR+zmc/OlD3NkcJeyyG7fy3672flgTbv1WzkUKgcX3+dU0/HjsAftX0lFbKrr39zuIavogTIcmBHuVytKicfeLFCqvTneoGpXZqvkVhEaY/2qFbXpCc5Akw1oiMz1iifJ1G", "targets": [ - { "target": "ticket_sidebar", "entrypoint": "index.html" }, - { "target": "user_sidebar", "entrypoint": "index.html" } + { + "target": "ticket_sidebar", + "entrypoint": "index.html" + }, + { + "target": "user_sidebar", + "entrypoint": "index.html" + } ], "entityAssociations": { "linkedShopifyCustomers": { @@ -20,38 +27,100 @@ } }, "settings": { + "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 + }, "shop_name": { "title": "Shop name", "description": "Your Shop name", "type": "string", "isRequired": true, "isBackendOnly": false, - "order": 10 + "order": 20 }, "access_token": { "title": "Access Token", "description": "Your shopify access token, please follow the app setup guide to get this", "type": "string", - "isRequired": true, + "isRequired": false, "isBackendOnly": true, - "order": 20 + "condition": "settings.use_advanced_connect != false && settings.use_access_token == true", + "order": 30 }, "verify_settings": { "title": "", "type": "app_embedded", - "options": { "entrypoint": "#/admin/verify_settings" }, + "options": { + "entrypoint": "#/admin/verify_settings", + "height": "30px" + }, "isRequired": false, + "condition": "settings.use_advanced_connect != false && settings.use_access_token == true", "isBackendOnly": true, - "order": 30 + "order": 40 + }, + "client_id": { + "title": "Client ID", + "type": "string", + "isRequired": false, + "isBackendOnly": false, + "condition": "settings.use_advanced_connect != false && settings.use_access_token != true", + "order": 50 + }, + "client_secret": { + "title": "Client Secret", + "type": "string", + "isRequired": false, + "isBackendOnly": true, + "condition": "settings.use_advanced_connect != false && settings.use_access_token != true", + "order": 60 + }, + "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": 70 } }, "proxy": { "whitelist": [ { "url": "https://(.*).myshopify.com/admin/api/.*", - "methods": ["GET", "POST", "PUT", "DELETE"], + "methods": [ + "GET", + "POST", + "PUT", + "DELETE" + ], + "timeout": 30 + }, + { + "url": "https://(.*).myshopify.com/admin/oauth/.*", + "methods": [ + "POST" + ], "timeout": 30 } ] } -} +} \ No newline at end of file diff --git a/package.json b/package.json index b40a412..3263dce 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,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", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@heroicons/react": "1.0.6", @@ -74,6 +74,6 @@ "slugify": "^1.6.6", "ts-jest": "^29.2.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 1b3afa4..db8dbbc 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)) @@ -181,8 +181,8 @@ importers: specifier: ^5.7.2 version: 5.7.2 vite: - specifier: ^6.0.11 - version: 6.0.11(@types/node@22.10.5)(jiti@2.4.2)(sass@1.83.1)(yaml@2.7.0) + specifier: ^6.2.2 + version: 6.2.2(@types/node@22.10.5)(jiti@2.4.2)(sass@1.84.0)(yaml@2.7.0) packages: @@ -560,8 +560,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 @@ -602,152 +602,152 @@ packages: resolution: {integrity: sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==} engines: {node: '>=18.0.0'} - '@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] @@ -2254,8 +2254,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 @@ -3096,8 +3096,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==} @@ -3455,8 +3455,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: @@ -3719,8 +3719,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 @@ -4140,8 +4140,8 @@ packages: resolution: {integrity: sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==} engines: {node: '>=12'} - 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: @@ -4774,7 +4774,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 @@ -4790,7 +4790,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 @@ -4799,7 +4799,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 @@ -4863,79 +4863,79 @@ snapshots: dependencies: tslib: 2.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-community/eslint-utils@4.4.1(eslint@8.4.1)': @@ -6829,33 +6829,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: {} @@ -7984,7 +7984,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: {} @@ -8312,7 +8312,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 @@ -8596,7 +8596,7 @@ snapshots: safer-buffer@2.1.2: {} - sass@1.83.1: + sass@1.84.0: dependencies: chokidar: 4.0.3 immutable: 5.0.3 @@ -8997,16 +8997,16 @@ snapshots: value-or-promise@1.0.12: {} - vite@6.0.11(@types/node@22.10.5)(jiti@2.4.2)(sass@1.83.1)(yaml@2.7.0): + vite@6.2.2(@types/node@22.10.5)(jiti@2.4.2)(sass@1.84.0)(yaml@2.7.0): dependencies: - esbuild: 0.24.2 - postcss: 8.4.49 + esbuild: 0.25.1 + postcss: 8.5.3 rollup: 4.31.0 optionalDependencies: '@types/node': 22.10.5 fsevents: 2.3.3 jiti: 2.4.2 - sass: 1.83.1 + sass: 1.84.0 yaml: 2.7.0 w3c-xmlserializer@4.0.0: diff --git a/src/App.tsx b/src/App.tsx index 136cfd4..43ad3f9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,11 @@ +import { AdminCallbackPage, EditCustomerPage, EditOrderPage, HomePage, LinkCustomerPage, ListOrdersPage, LoadingAppPage, LoginPage, LogoutPage, VerifySettings, ViewCustomerPage, ViewOrderPage } from "./pages"; +import { isNavigatePayload } from "./utils"; +import { match } from "ts-pattern"; import { Routes, Route, useNavigate } from "react-router-dom"; import { useDebouncedCallback } from "use-debounce"; -import { match } from "ts-pattern"; -import { - useDeskproAppClient, - useDeskproAppEvents, -} from "@deskpro/app-sdk"; -import { isNavigatePayload } from "./utils"; -import { - HomePage, - ViewOrderPage, - EditOrderPage, - LoadingAppPage, - ListOrdersPage, - VerifySettings, - EditCustomerPage, - ViewCustomerPage, - LinkCustomerPage, -} from "./pages"; -import type { FC } from "react"; +import { useDeskproAppClient, useDeskproAppEvents } from "@deskpro/app-sdk"; import type { EventPayload } from "./types"; +import type { FC } from "react"; const App: FC = () => { const navigate = useNavigate(); @@ -41,15 +28,18 @@ const App: FC = () => { return ( - } /> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> ); } diff --git a/src/constants.ts b/src/constants.ts index c2f1ccb..be06a7a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,6 +8,9 @@ export const ENTITY = 'linkedShopifyCustomers'; export const DEFAULT_ERROR = "There was an error!"; +export const OAUTH2_ACCESS_TOKEN_PATH = "oauth2/access_token" +export const OAUTH2_REFRESH_TOKEN_PATH = "oauth2/refresh_token" + export const placeholders = { SHOP_NAME: "__shop_name__", ACCESS_TOKEN: "__access_token__", diff --git a/src/pages/AdminCallbackPage/AdminCallbackPage.tsx b/src/pages/AdminCallbackPage/AdminCallbackPage.tsx new file mode 100644 index 0000000..02cea1c --- /dev/null +++ b/src/pages/AdminCallbackPage/AdminCallbackPage.tsx @@ -0,0 +1,53 @@ +import { CopyToClipboardInput, LoadingSpinner, OAuth2Result, useDeskproLatestAppContext, useInitialisedDeskproAppClient, } from "@deskpro/app-sdk"; +import { createSearchParams } from "react-router-dom"; +import { P1 } from "@deskpro/deskpro-ui"; +import { Settings } from "@/types"; +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) + const {context} = useDeskproLatestAppContext() + + useInitialisedDeskproAppClient(async (client) => { + const oauth2 = await client.startOauth2Local( + ({ callbackUrl, state }) => { + return `https://${context?.settings.shop_name}.myshopify.com/admin/oauth2/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 Shopify app setup + + ); +}; + +export { AdminCallbackPage }; diff --git a/src/pages/AdminCallbackPage/index.ts b/src/pages/AdminCallbackPage/index.ts new file mode 100644 index 0000000..2f4f193 --- /dev/null +++ b/src/pages/AdminCallbackPage/index.ts @@ -0,0 +1 @@ +export { AdminCallbackPage } from "./AdminCallbackPage"; diff --git a/src/pages/LinkCustomerPage/LinkCustomerPage.tsx b/src/pages/LinkCustomerPage/LinkCustomerPage.tsx index 0eff23f..314bbbd 100644 --- a/src/pages/LinkCustomerPage/LinkCustomerPage.tsx +++ b/src/pages/LinkCustomerPage/LinkCustomerPage.tsx @@ -1,5 +1,4 @@ -import {useMemo, useState, useEffect, useCallback} from "react"; -import get from "lodash/get"; +import { useState, useEffect, useCallback} from "react"; import isEmpty from "lodash/isEmpty"; import { useDebouncedCallback } from "use-debounce"; import { useNavigate, useSearchParams } from "react-router-dom"; @@ -17,25 +16,25 @@ import { useSearch } from "./hooks"; import { LinkCustomer } from "../../components"; import type { FC, ChangeEvent } from "react"; import type { CustomerType } from "../../services/shopify/types"; +import { ContextData, Settings } from "@/types"; const LinkCustomerPage: FC = () => { - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const { client } = useDeskproAppClient(); - const { context } = useDeskproLatestAppContext(); - const [search, setSearch] = useState(""); - const [selectedCustomerId, setSelectedCustomerId] = useState(""); const [isEditMode, setIsEditMode] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); + const [search, setSearch] = useState(""); + const [searchParams] = useSearchParams(); + const [selectedCustomerId, setSelectedCustomerId] = useState(""); + const { client } = useDeskproAppClient(); + const { context } = useDeskproLatestAppContext(); const { isLoading, customers } = useSearch(search); - const customerId = useMemo(() => searchParams.get("customerId"), [searchParams]); - const dpUser = useMemo(() => { - return get(context, ["data", "ticket", "primaryUser"]) - || get(context, ["data", "user"]) - }, [context]); + const customerId = searchParams.get("customerId") + const dpUser = context?.data?.ticket?.primaryUser || context?.data?.user + const navigate = useNavigate(); useSetTitle("Link Customer"); + const isUsingOAuth = context?.settings?.use_access_token !== true || context.settings.use_advanced_connect === false + useRegisterElements(({ registerElement }) => { registerElement("refresh", { type: "refresh_button" }); @@ -45,6 +44,19 @@ const LinkCustomerPage: FC = () => { payload: { type: "changePage", path: "/home" } }); } + + if (isUsingOAuth) { + registerElement("menu", { + type: "menu", + items: [{ + title: "Logout", + payload: { + type: "changePage", + path: `/logout`, + }, + }], + }); + } }, [isEditMode]); useEffect(() => { diff --git a/src/pages/LoadingAppPage/LoadingAppPage.tsx b/src/pages/LoadingAppPage/LoadingAppPage.tsx index 610146f..a17921f 100644 --- a/src/pages/LoadingAppPage/LoadingAppPage.tsx +++ b/src/pages/LoadingAppPage/LoadingAppPage.tsx @@ -1,22 +1,85 @@ -import { FC } from "react"; -import { LoadingSpinner } from "@deskpro/app-sdk"; +import { ErrorBlock } from "@/components/common"; +import { FC, useState } from "react"; +import { getEntityCustomerList } from "@/services/deskpro"; +import { getShopInfo } from "@/services/shopify"; +import { LoadingSpinner, useDeskproAppClient, useDeskproElements, useDeskproLatestAppContext, useInitialisedDeskproAppClient } from "@deskpro/app-sdk"; +import { Settings, ContextData } from "@/types"; +import { Stack } from "@deskpro/deskpro-ui"; import { useNavigate } from "react-router-dom"; -import { useTryToLinkCustomer, useRegisterElements } from "../../hooks"; const LoadingAppPage: FC = () => { + const { client } = useDeskproAppClient() + const { context } = useDeskproLatestAppContext() + + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isFetchingAuth, setIsFetchingAuth] = useState(true) + const navigate = useNavigate(); - useRegisterElements(({ registerElement }) => { - registerElement("refresh", { type: "refresh_button" }); + // 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("refresh", { type: "refresh_button" }) }); - useTryToLinkCustomer( - () => navigate("/home"), - () => navigate("/link_customer"), - ); + useInitialisedDeskproAppClient((client) => { + client.setTitle("Shopify") + + 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 + getShopInfo(client) + .then((result) => { + if (result?.data?.shop) { + setIsAuthenticated(true) + } + }) + .catch(() => { }) + .finally(() => { + setIsFetchingAuth(false) + }) + }, [context, context?.settings]) + + if (!client || !user || isFetchingAuth) { + return () + } + if (isAuthenticated) { + + getEntityCustomerList(client, user.id) + .then((customers) => { + customers.length < 1 ? navigate("/link_customer") : + navigate("/home") + }) + .catch(() => {}) + } else { + + if (isUsingOAuth) { + navigate("/login") + } else { + // Show error for invalid access tokens (expired or not present) + return ( + + + + ) + } + + } + + return ( - + ); }; diff --git a/src/pages/Login/LoginPage.tsx b/src/pages/Login/LoginPage.tsx new file mode 100644 index 0000000..a33f62d --- /dev/null +++ b/src/pages/Login/LoginPage.tsx @@ -0,0 +1,36 @@ +import { AnchorButton, H3, Stack } from "@deskpro/deskpro-ui" +import { ErrorBlock } from "@/components/common" +import { FC } from "react" +import { useDeskproElements, useInitialisedDeskproAppClient } from "@deskpro/app-sdk" +import useLogin from "./useLogin" + +const LoginPage: FC = () => { + useDeskproElements(({ registerElement, clearElements }) => { + clearElements() + registerElement("refresh", { type: "refresh_button" }) + }) + + useInitialisedDeskproAppClient((client)=>{ + client.setTitle("Login") + }, []) + + const { onSignIn, authUrl, isLoading, error } = useLogin(); + + return ( + +

Log into your Shopify 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..7cc2155 --- /dev/null +++ b/src/pages/Login/useLogin.ts @@ -0,0 +1,135 @@ +import { ContextData, Settings } from "@/types"; +import { createSearchParams, useNavigate } from "react-router-dom"; +import { getAccessToken, getShopInfo } from "@/services/shopify"; +import { getEntityCustomerList } from "@/services/deskpro"; +import { IOAuth2, OAuth2Result, useDeskproLatestAppContext, useInitialisedDeskproAppClient } from "@deskpro/app-sdk"; +import { OAUTH2_ACCESS_TOKEN_PATH, OAUTH2_REFRESH_TOKEN_PATH } from "@/constants"; +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 isUsingOAuth = context?.settings?.use_access_token !== true || context.settings.use_advanced_connect === false + const user = context?.data?.ticket?.primaryUser || context?.data?.user + + 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') { + // Local mode requires a clientId. + setError("A client ID is required"); + return + } + const oAuth2Response = mode === "local" ? + await client.startOauth2Local( + ({ state, callbackUrl }) => { + return `https://${context?.settings.shop_name}.myshopify.com/admin/oauth/authorize?${createSearchParams([ + ["client_id", clientId ?? ""], + ["state", state], + ["redirect_uri", callbackUrl], + ["scope", "read_inventory,write_assigned_fulfillment_orders,read_assigned_fulfillment_orders,write_customers,read_customers,write_draft_orders,read_draft_orders,write_order_edits,read_order_edits,write_orders,read_orders,write_product_listings,read_product_listings,write_products,read_products"] + ])}` + }, + /\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); + + return { data } + } + ) + // Global Proxy Service + : await client.startOauth2Global("0ad23fa9caf394119372cd5db27dba4b"); + + setAuthUrl(mode === "local" ? oAuth2Response.authorizationUrl : `${oAuth2Response.authorizationUrl}&subdomain=${context.settings.shop_name}`) + 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 }) + } + + + try { + const shopResult = await getShopInfo(client) + + if (!shopResult?.data?.shop) { + throw new Error() + } + } catch { + throw new Error("Error authenticating user") + } + + + getEntityCustomerList(client, user.id) + .then((customers) => { + customers.length < 1 ? navigate("/link_customer") : + navigate("/home") + }) + .catch(() => { navigate("/link_customer") }) + } 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/LogoutPage/LogoutPage.tsx b/src/pages/LogoutPage/LogoutPage.tsx new file mode 100644 index 0000000..c84fffd --- /dev/null +++ b/src/pages/LogoutPage/LogoutPage.tsx @@ -0,0 +1,22 @@ +import { FC } from "react"; +import { LoadingSpinner, useInitialisedDeskproAppClient } from "@deskpro/app-sdk"; +import { OAUTH2_ACCESS_TOKEN_PATH } from "@/constants"; +import { useNavigate } from "react-router-dom"; + +const LogoutPage: FC = () => { + const navigate = useNavigate(); + + useInitialisedDeskproAppClient((client) => { + client.setBadgeCount(0) + + client.deleteUserState(OAUTH2_ACCESS_TOKEN_PATH) + .catch(() => { }) + .finally(() => { + navigate("/login"); + }); + }, [navigate]) + + return () +} + +export default LogoutPage \ No newline at end of file diff --git a/src/pages/LogoutPage/index.ts b/src/pages/LogoutPage/index.ts new file mode 100644 index 0000000..8efcfe6 --- /dev/null +++ b/src/pages/LogoutPage/index.ts @@ -0,0 +1 @@ +export { default } from "./LogoutPage"; diff --git a/src/pages/ViewCustomerPage/ViewCustomerPage.tsx b/src/pages/ViewCustomerPage/ViewCustomerPage.tsx index bfb163c..2b9642d 100644 --- a/src/pages/ViewCustomerPage/ViewCustomerPage.tsx +++ b/src/pages/ViewCustomerPage/ViewCustomerPage.tsx @@ -1,69 +1,81 @@ +import { ContextData, Settings } from "@/types"; +import { useCustomer } from "../../hooks"; import { useMemo } from "react"; import { useSearchParams } from "react-router-dom"; -import { - LoadingSpinner, - useDeskproAppTheme, - useDeskproAppClient, -} from "@deskpro/app-sdk"; -import { - useSetTitle, - useExternalLink, - useRegisterElements, -} from "../../hooks"; -import { useCustomer } from "../../hooks"; import { ViewCustomer } from "../../components"; +import { LoadingSpinner, useDeskproAppClient, useDeskproAppTheme, useDeskproElements, useDeskproLatestAppContext } from "@deskpro/app-sdk"; +import { useExternalLink, useSetTitle } from "../../hooks"; import type { FC } from "react"; const ViewCustomerPage: FC = () => { - const [searchParams] = useSearchParams(); - const customerId = searchParams.get("customerId"); - const { theme } = useDeskproAppTheme(); - const { client } = useDeskproAppClient(); - const { getCustomerLink } = useExternalLink(); - const { isLoading, customer } = useCustomer(customerId); - const customerLink = useMemo(() => { - return getCustomerLink(customer?.legacyResourceId); - }, [getCustomerLink, customer?.legacyResourceId]); + const [searchParams] = useSearchParams(); + const customerId = searchParams.get("customerId"); + const { theme } = useDeskproAppTheme(); + const { client } = useDeskproAppClient(); + const { getCustomerLink } = useExternalLink(); + const { context } = useDeskproLatestAppContext() + const { isLoading, customer } = useCustomer(customerId); + const customerLink = useMemo(() => { + return getCustomerLink(customer?.legacyResourceId); + }, [getCustomerLink, customer?.legacyResourceId]); - useSetTitle(customer?.displayName || "Shopify"); + const isUsingOAuth = context?.settings?.use_access_token !== true || context.settings.use_advanced_connect === false - useRegisterElements(({ registerElement }) => { - registerElement("refresh", { type: "refresh_button" }); - registerElement("home", { - type: "home_button", - payload: { type: "changePage", path: "/home" } - }); - registerElement("edit", { - type: "edit_button", - payload: { - type: "changePage", - path: { - pathname: "/edit_customer", - search: `?customerId=${customer?.id}`, - } - }, - }); + useSetTitle(customer?.displayName || "Shopify"); - if (customerLink) { - registerElement("external", { - type: "cta_external_link", - url: customerLink, - hasIcon: true, - }); + useDeskproElements(({ registerElement, clearElements }) => { + clearElements() + registerElement("refresh", { type: "refresh_button" }) + registerElement("home", { + type: "home_button", + payload: { type: "changePage", path: "/home" } + }); + + registerElement("edit", { + type: "edit_button", + payload: { + type: "changePage", + path: { + pathname: "/edit_customer", + search: `?customerId=${customer?.id}`, } - }, [client, customer]); + }, + }) + if (customerLink) { + registerElement("external", { + type: "cta_external_link", + url: customerLink, + hasIcon: true, + }); + } - if (isLoading) { - return + if (isUsingOAuth) { + registerElement("menu", { + type: "menu", + items: [{ + title: "Logout", + payload: { + type: "changePage", + path: `/logout`, + }, + }], + }); } - return ( - - ); + }, [client, customer, context, context?.settings]); + + + if (isLoading) { + return + } + + return ( + + ); }; export { ViewCustomerPage }; diff --git a/src/pages/index.ts b/src/pages/index.ts index 1678ab4..f3bb88c 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,9 +1,12 @@ +export { AdminCallbackPage } from "./AdminCallbackPage"; +export { default as LoginPage } from "./Login"; +export { default as LogoutPage } from "./LogoutPage"; export { EditCustomerPage } from "./EditCustomerPage"; export { EditOrderPage } from "./EditOrderPage"; export { HomePage } from "./HomePage"; export { LinkCustomerPage } from "./LinkCustomerPage"; export { ListOrdersPage } from "./ListOrdersPage"; export { LoadingAppPage } from "./LoadingAppPage"; +export { VerifySettings } from "./VerifySettings"; export { ViewCustomerPage } from "./ViewCustomerPage"; export { ViewOrderPage } from "./ViewOrderPage"; -export { VerifySettings } from "./VerifySettings"; diff --git a/src/services/shopify/baseGraphQLRequest.ts b/src/services/shopify/baseGraphQLRequest.ts index 27343fb..252483c 100644 --- a/src/services/shopify/baseGraphQLRequest.ts +++ b/src/services/shopify/baseGraphQLRequest.ts @@ -1,11 +1,11 @@ +import { getQueryParams } from "@/utils"; +import { GRAPHQL_URL, OAUTH2_ACCESS_TOKEN_PATH, placeholders } from "@/constants"; +import { proxyFetch, adminGenericProxyFetch } from "@deskpro/app-sdk"; +import { ShopifyError } from "./ShopifyError"; import has from "lodash/has"; import isEmpty from "lodash/isEmpty"; import isString from "lodash"; -import { proxyFetch, adminGenericProxyFetch } from "@deskpro/app-sdk"; -import { GRAPHQL_URL, placeholders } from "../../constants"; -import { getQueryParams } from "../../utils"; -import { ShopifyError } from "./ShopifyError"; -import type { Request } from "../../types"; +import type { Request } from "@/types"; const baseGraphQLRequest: Request = async (client, { url, @@ -22,13 +22,15 @@ const baseGraphQLRequest: Request = async (client, { const baseUrl = rawUrl ? rawUrl : `${GRAPHQL_URL(settings?.shop_name)}${url || ""}`; const params = getQueryParams(queryParams); + const isUsingOAuth2 = (await client.getUserState("isUsingOAuth"))[0].data + const requestUrl = `${baseUrl}${isEmpty(params) ? "": `?${params}`}`; const options: RequestInit = { method, headers: { "Accept": "application/json", "Content-Type": "application/json", - "X-Shopify-Access-Token": settings?.access_token || placeholders.ACCESS_TOKEN, + "X-Shopify-Access-Token": settings?.access_token || (isUsingOAuth2 ? `[user[${OAUTH2_ACCESS_TOKEN_PATH}]]` : placeholders.ACCESS_TOKEN) , ...customHeaders, }, }; diff --git a/src/services/shopify/getAccessToken.ts b/src/services/shopify/getAccessToken.ts new file mode 100644 index 0000000..3d500f6 --- /dev/null +++ b/src/services/shopify/getAccessToken.ts @@ -0,0 +1,30 @@ +import { placeholders } from "@/constants"; +import { IDeskproClient, proxyFetch } from "@deskpro/app-sdk"; + +export default async function getAccessToken( + client: IDeskproClient, + code: string, +) { + try { + const fetch = await proxyFetch(client); + + const response = await fetch(`https://${placeholders.SHOP_NAME}.myshopify.com/admin/oauth/access_token`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + client_id: "__client_id__", + client_secret: "__client_secret__", + code: code + }) + }); + + 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"); + } +} diff --git a/src/services/shopify/index.ts b/src/services/shopify/index.ts index a05126e..e005d88 100644 --- a/src/services/shopify/index.ts +++ b/src/services/shopify/index.ts @@ -1,7 +1,8 @@ -export { ShopifyError } from "./ShopifyError"; -export { getShopInfo } from "./getShopInfo"; -export { getCustomers } from "./getCustomers"; export { getCustomer } from "./getCustomer"; +export { getCustomers } from "./getCustomers"; export { getOrder } from "./getOrder"; +export { getShopInfo } from "./getShopInfo"; export { setCustomer } from "./setCustomer"; export { setOrder } from "./setOrder"; +export { ShopifyError } from "./ShopifyError"; +export { default as getAccessToken } from "./getAccessToken" diff --git a/src/types.ts b/src/types.ts index a433428..28f2a36 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,6 +28,34 @@ export type Request = ( export type Settings = { shop_name?: string, access_token?: string, + 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[] + } }; export type NavigateToChangePage = { type: "changePage", path: To }; 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 5442ca8..b0e99ed 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) {