diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..2a006ea --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @transferwise/wp-devex diff --git a/common/db/index.ts b/common/db/index.ts index f27c762..01eb3bc 100644 --- a/common/db/index.ts +++ b/common/db/index.ts @@ -1,6 +1,7 @@ import JSONdb from '../lib/Simple-JSONdb'; import type { Config } from '../types/Config'; +import type { SelectedProfile } from '../types/SelectedProfile'; import type { Tokens } from '../types/Tokens'; const ONE_HOUR_IN_MS = 60 * 60 * 1000; @@ -8,10 +9,21 @@ const ONE_HOUR_IN_MS = 60 * 60 * 1000; // Instead of proper database we have a simple JSON file export const store = new JSONdb('../storage.json'); -export const getSelectedWiseProfileId = () => store.get('selectedProfileId') as string; +export const getSelectedWiseProfileId = () => { + const selectedProfile = store.get('selectedProfile'); + if (!selectedProfile) { + return null; + } + return (selectedProfile as SelectedProfile).id; +} export const getWiseEnvironmentConfig = () => store.get('config') as Config; +export const getWiseOAuthPageURL = () => { + const config = getWiseEnvironmentConfig(); + return `${config.oauthPageUrl}?client_id=${config.clientId}&redirect_uri=${config.redirectUri}`; +}; + export const getWiseTokens = () => { const selectedProfileId = getSelectedWiseProfileId(); if (!selectedProfileId) { diff --git a/common/server/exchangeAuthCodeForToken.ts b/common/server/exchangeAuthCodeForToken.ts index 84aa3a5..6894700 100644 --- a/common/server/exchangeAuthCodeForToken.ts +++ b/common/server/exchangeAuthCodeForToken.ts @@ -37,7 +37,7 @@ export const exchangeAuthCodeForToken = async ( // Stores Wise profileId and tokens in our demo database (storage.json). // In a production environment, you would associate these tokens with a logged-in user // in your system. - store.set('selectedProfileId', profileId); + store.set('selectedProfile', { id: profileId }); store.set(profileId, { accessToken: result.access_token, refreshToken: result.refresh_token, diff --git a/common/server/fetchProfileDetails.ts b/common/server/fetchProfileDetails.ts index dc65450..b6cb0da 100644 --- a/common/server/fetchProfileDetails.ts +++ b/common/server/fetchProfileDetails.ts @@ -3,6 +3,9 @@ import { getSelectedWiseProfileId, getWiseEnvironmentConfig, getWiseAccessToken export const fetchProfileDetails = async () => { const config = getWiseEnvironmentConfig(); const selectedProfileId = getSelectedWiseProfileId(); + if (!selectedProfileId) { + throw Error('No selectedProfileId'); + } const oauthToken = getWiseAccessToken(); const headers = new Headers(); headers.set('Authorization', `Bearer ${oauthToken}`); diff --git a/common/server/refreshWiseToken.ts b/common/server/refreshWiseToken.ts index 5e5d9c2..6b2c225 100644 --- a/common/server/refreshWiseToken.ts +++ b/common/server/refreshWiseToken.ts @@ -6,6 +6,9 @@ export const refreshWiseToken = async () => { const config = getWiseEnvironmentConfig(); const refreshToken = getWiseRefreshToken(); const selectedProfileId = getSelectedWiseProfileId(); + if (!selectedProfileId) { + throw Error('No selectedProfileId'); + } const headers = new Headers(); headers.set('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8'); headers.set( diff --git a/common/types/SelectedProfile.ts b/common/types/SelectedProfile.ts new file mode 100644 index 0000000..efd1711 --- /dev/null +++ b/common/types/SelectedProfile.ts @@ -0,0 +1,3 @@ +export type SelectedProfile = { + id: string +}; diff --git a/oauth-connect-popup/.gitignore b/oauth-connect-popup/.gitignore new file mode 100644 index 0000000..2989402 --- /dev/null +++ b/oauth-connect-popup/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +**/.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/oauth-connect-popup/README.md b/oauth-connect-popup/README.md new file mode 100644 index 0000000..785c9ad --- /dev/null +++ b/oauth-connect-popup/README.md @@ -0,0 +1,34 @@ +# OAuth Connect Popup Sample + +In our regular authorization flow your application redirects the customer to the Wise authorization page. With some additional steps it can be implemented inside a popup window, meaning that the user does not leave your app. + +On a high level you'll only need to do two changes: +- open the Wise authorization page in a popup window instead of a full redirect +- build a mechanism that understands when the flow is complete + +See sequence diagram below for more details. + +More info about [Authentication and access](https://docs.wise.com/api-docs/features/authentication-access) and [Authorization flow in a Popup Window](https://docs.wise.com/api-docs/guides/oauth-popup). + +## Key elements + +We're using [Next.js](https://nextjs.org/), because it allows us to show server and client side code in a single app. + +Start off by exploring `/pages/index.tsx`. + +## Running it locally + +Prerequisite: make sure you have [Node.js](https://nodejs.org/en) installed. +- `npm install` (installs all the packages) +- `npm run dev` (starts the app at http://localhost:3000/) + +## Recording + +https://github.com/transferwise/wise-platform-samples/assets/39053304/b9b8c4a8-7e42-4b4b-99b0-204ebfe91ec2 + +## Sequence diagram + +![oauth-consent-popup](https://github.com/transferwise/wise-platform-samples/assets/39053304/ffee4fd7-7d1b-46a4-97ea-b67fcaf54b94) + +For more details have a look at our [Authorization flow in a Popup Window](https://docs.wise.com/api-docs/guides/oauth-popup) guide. + diff --git a/oauth-connect-popup/next.config.js b/oauth-connect-popup/next.config.js new file mode 100644 index 0000000..0d96e9c --- /dev/null +++ b/oauth-connect-popup/next.config.js @@ -0,0 +1,7 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + transpilePackages: ['common'], +} + +module.exports = nextConfig diff --git a/oauth-connect-popup/package-lock.json b/oauth-connect-popup/package-lock.json new file mode 100644 index 0000000..9356a1e --- /dev/null +++ b/oauth-connect-popup/package-lock.json @@ -0,0 +1,713 @@ +{ + "name": "oauth-connect-popup", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "oauth-connect-popup", + "version": "0.1.0", + "dependencies": { + "@types/node": "^20.6.2", + "@types/react": "^18.2.21", + "@types/react-dom": "^18.2.7", + "next": "^13.4.19", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.2.2" + } + }, + "node_modules/@next/env": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz", + "integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz", + "integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz", + "integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz", + "integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz", + "integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz", + "integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz", + "integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz", + "integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz", + "integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz", + "integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", + "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.8", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz", + "integrity": "sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==" + }, + "node_modules/@types/react": { + "version": "18.2.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.24.tgz", + "integrity": "sha512-Ee0Jt4sbJxMu1iDcetZEIKQr99J1Zfb6D4F3qfUWoR1JpInkY1Wdg4WwCyBjL257D0+jGqSl1twBjV8iCaC0Aw==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", + "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001543", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz", + "integrity": "sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz", + "integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==", + "dependencies": { + "@next/env": "13.5.4", + "@swc/helpers": "0.5.2", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.31", + "styled-jsx": "5.1.1", + "watchpack": "2.4.0" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=16.14.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "13.5.4", + "@next/swc-darwin-x64": "13.5.4", + "@next/swc-linux-arm64-gnu": "13.5.4", + "@next/swc-linux-arm64-musl": "13.5.4", + "@next/swc-linux-x64-gnu": "13.5.4", + "@next/swc-linux-x64-musl": "13.5.4", + "@next/swc-win32-arm64-msvc": "13.5.4", + "@next/swc-win32-ia32-msvc": "13.5.4", + "@next/swc-win32-x64-msvc": "13.5.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + } + }, + "dependencies": { + "@next/env": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz", + "integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==" + }, + "@next/swc-darwin-arm64": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz", + "integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==", + "optional": true + }, + "@next/swc-darwin-x64": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz", + "integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==", + "optional": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz", + "integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==", + "optional": true + }, + "@next/swc-linux-arm64-musl": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz", + "integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==", + "optional": true + }, + "@next/swc-linux-x64-gnu": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz", + "integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==", + "optional": true + }, + "@next/swc-linux-x64-musl": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz", + "integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==", + "optional": true + }, + "@next/swc-win32-arm64-msvc": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz", + "integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==", + "optional": true + }, + "@next/swc-win32-ia32-msvc": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz", + "integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==", + "optional": true + }, + "@next/swc-win32-x64-msvc": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz", + "integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==", + "optional": true + }, + "@swc/helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", + "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@types/node": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", + "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==" + }, + "@types/prop-types": { + "version": "15.7.8", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz", + "integrity": "sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==" + }, + "@types/react": { + "version": "18.2.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.24.tgz", + "integrity": "sha512-Ee0Jt4sbJxMu1iDcetZEIKQr99J1Zfb6D4F3qfUWoR1JpInkY1Wdg4WwCyBjL257D0+jGqSl1twBjV8iCaC0Aw==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.8.tgz", + "integrity": "sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw==", + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==" + }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001543", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz", + "integrity": "sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==" + }, + "client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + }, + "next": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz", + "integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==", + "requires": { + "@next/env": "13.5.4", + "@next/swc-darwin-arm64": "13.5.4", + "@next/swc-darwin-x64": "13.5.4", + "@next/swc-linux-arm64-gnu": "13.5.4", + "@next/swc-linux-arm64-musl": "13.5.4", + "@next/swc-linux-x64-gnu": "13.5.4", + "@next/swc-linux-x64-musl": "13.5.4", + "@next/swc-win32-arm64-msvc": "13.5.4", + "@next/swc-win32-ia32-msvc": "13.5.4", + "@next/swc-win32-x64-msvc": "13.5.4", + "@swc/helpers": "0.5.2", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.31", + "styled-jsx": "5.1.1", + "watchpack": "2.4.0" + } + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, + "styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "requires": { + "client-only": "0.0.1" + } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==" + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + } + } +} diff --git a/oauth-connect-popup/package.json b/oauth-connect-popup/package.json new file mode 100644 index 0000000..3eb09c0 --- /dev/null +++ b/oauth-connect-popup/package.json @@ -0,0 +1,20 @@ +{ + "name": "oauth-connect-popup", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@types/node": "^20.6.2", + "@types/react": "^18.2.21", + "@types/react-dom": "^18.2.7", + "next": "^13.4.19", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.2.2" + } +} diff --git a/oauth-connect-popup/pages/_app.tsx b/oauth-connect-popup/pages/_app.tsx new file mode 100644 index 0000000..b3fbe17 --- /dev/null +++ b/oauth-connect-popup/pages/_app.tsx @@ -0,0 +1,7 @@ +import type { AppProps } from 'next/app'; + +import '../../common/styles/globals.css'; + +export default function App({ Component, pageProps }: AppProps) { + return ; +} diff --git a/oauth-connect-popup/pages/_document.tsx b/oauth-connect-popup/pages/_document.tsx new file mode 100644 index 0000000..e1e9cbb --- /dev/null +++ b/oauth-connect-popup/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from 'next/document'; + +export default function Document() { + return ( + + + +
+ + + + ); +} diff --git a/oauth-connect-popup/pages/api/profile.ts b/oauth-connect-popup/pages/api/profile.ts new file mode 100644 index 0000000..9b45081 --- /dev/null +++ b/oauth-connect-popup/pages/api/profile.ts @@ -0,0 +1,38 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { + getSelectedWiseProfileId, + isWiseTokenAboutToExpire, +} from '../../../common/db'; +import { fetchProfileDetails } from '../../../common/server/fetchProfileDetails'; +import { refreshWiseToken } from '../../../common/server/refreshWiseToken'; + +type ResponseData = { + message: string; +}; + +// This code runs on server side (on your backend). +// We check if Wise account has been connected and return Wise profile details +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + // Check if accounts have been connected before (based on if selected profile exists) + const isWiseAccountConnected = Boolean(getSelectedWiseProfileId()); + + // Wise account not connected, so cannot call Wise API + if (!isWiseAccountConnected) { + res.statusCode = 404; + res.json({ message: 'Wise account not connected' }); + return; + } + + // Generate new token if it is expired + if (isWiseTokenAboutToExpire()) { + await refreshWiseToken(); + } + + // We have a valid token, so we can proceed with the call to Wise API + const profile = await fetchProfileDetails(); + res.statusCode = 200; + res.json(profile); +} diff --git a/oauth-connect-popup/pages/env-setup.tsx b/oauth-connect-popup/pages/env-setup.tsx new file mode 100644 index 0000000..07be536 --- /dev/null +++ b/oauth-connect-popup/pages/env-setup.tsx @@ -0,0 +1,119 @@ +// Special page for setting up environment vars. You wouldn't need it on your app! +import { GetServerSideProps } from 'next'; +import Head from 'next/head'; +import { useState } from 'react'; + +import { DEFAULT_REDIRECT_URI } from '../../common/const'; +import { store } from '../../common/db'; +import { getPostBodyAsURLSearchParams } from '../../common/utils/getPostBodyAsURLSearchParams'; + +// This function runs only on the server side. +// If form gets submitted, we store the variables in our mock data store +export const getServerSideProps: GetServerSideProps = async ({ req }) => { + if (req.method === 'POST') { + const body = await getPostBodyAsURLSearchParams(req); + store.set('config', { + host: body.get('host'), + clientId: body.get('clientId'), + clientSecret: body.get('clientSecret'), + redirectUri: body.get('redirectUri'), + oauthPageUrl: body.get('oauthPageUrl'), + }); + return { + redirect: { + destination: '/', + permanent: false, + }, + }; + } + return { props: {} }; +}; + +export default function EnvSetupPage() { + const [redirectUri, setRedirectUri] = useState(DEFAULT_REDIRECT_URI); + return ( + <> + + Environment setup page + +
+
+ {/* Submitting basically reloads the same page, but now getServerSideProps will receive POST parameters */} +
+
+ Environment variables +

+ In order to make calls to the Wise API, it's necessary to + configure certain environment variables. +
+ Sample app stores those locally in + storage.json + {' '} + file (root dir). +
+ + Read more about authentication & access + + . +

+ + + + + + + + setRedirectUri(e.target.value)} + required + /> + {redirectUri !== DEFAULT_REDIRECT_URI && ( +

+ This sample app expects redirect URL to be{' '} + http://localhost:3000/wise-redirect.
+
+ You can still use the app though. Wise OAuth page redirects + you back to: +
+ {redirectUri}?code=123&profileId=223 +
+ You just have to copy the parameters and navigate to: +
+ + http://localhost:3000/wise-redirect?code=123&profileId=223 + +

+ )} + + + +
+
+
+
+ + ); +} diff --git a/oauth-connect-popup/pages/index.tsx b/oauth-connect-popup/pages/index.tsx new file mode 100644 index 0000000..488ada8 --- /dev/null +++ b/oauth-connect-popup/pages/index.tsx @@ -0,0 +1,103 @@ +import { GetServerSideProps } from 'next'; +import Head from 'next/head'; +import { FC, useState, useEffect } from 'react'; + +import { DEFAULT_REDIRECT_URI } from '../../common/const'; +import { getWiseEnvironmentConfig, getWiseOAuthPageURL } from '../../common/db'; +import { popupFlow } from '../src/popupHandler'; + +type PageProps = { + wiseOAuthPageURL: string; + redirectUri: string; +}; + +// Makes sure that Wise environment vars (clientId, clientSecret, redirectUri etc.) are set up. +export const getServerSideProps: GetServerSideProps = async () => { + const config = getWiseEnvironmentConfig(); // returns Wise API clientId, clientSecret, redirectUri etc. + + // Special page for setting up environment vars. You wouldn't need it on your app! + if (!config) { + return { redirect: { destination: '/env-setup', permanent: false } }; + } + + // Passes some props from backend to frontend (see below). Might not be relevant for your setup + const wiseOAuthPageURL = getWiseOAuthPageURL(); + return { props: { wiseOAuthPageURL, redirectUri: config.redirectUri } }; +}; + +// Frontend component of the page. +// Renders a button to connect Wise Account or your name if already connected. +const OAuthConnectPopupPage: FC = (props) => { + const [name, setName] = useState(); + + // Calls your backend to get Wise profile details (see /pages/api/profile.ts) + const getWiseProfile = () => { + fetch('/api/profile') + .then((response) => response.json()) + .then((result) => { + if (result.fullName) { + setName(result.fullName); + } + }); + }; + + // Runs on page load + useEffect(() => { + getWiseProfile(); + }, []); + + return ( + <> + + OAuth Connect Popup Sample + +
+
+ {name ? ( + <> +

Hello {name}!

+

Your account is successfully connected.

+ + ) : ( + <> +

OAuth Connect Popup Sample

+

+ You'll be redirected to Wise, where you can choose an account to + authorize access. +

+ {props.redirectUri !== DEFAULT_REDIRECT_URI && ( +

+ The redirectUri in your config is different than this sample + app uses. +
+ When Wise OAuth page redirects you back, please change the URL + within the popup to: +
+ + http://localhost:3000/wise-redirect?code=...&profileId=... + +

+ )} + + + )} +
+
+ + ); +}; + +export default OAuthConnectPopupPage; diff --git a/oauth-connect-popup/pages/wise-redirect.tsx b/oauth-connect-popup/pages/wise-redirect.tsx new file mode 100644 index 0000000..a534cdd --- /dev/null +++ b/oauth-connect-popup/pages/wise-redirect.tsx @@ -0,0 +1,39 @@ +import { GetServerSideProps } from 'next'; +import { useEffect } from 'react'; + +import { exchangeAuthCodeForToken } from '../../common/server/exchangeAuthCodeForToken'; + +const MESSAGE_SUCCESS = 'wise-oauth-success'; + +// We're letting the tab that opened popup (pages/index.ts) know that it can be closed now. +// There is no need to send any data using this way so just a +// message "we-are-done-and-you-can-close-me" is enough. +// https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage +const notifyOpener = (message: string) => { + if (window.opener) { + window.opener.postMessage(message, 'http://localhost:3000'); + } +}; + +// After granting consent on Wise website you will be redirected back to this page. +// The following block must run on server side. It reads the "code" and "profileId" parameters from the URL, +// then generates (and stores) Wise API tokens. +export const getServerSideProps: GetServerSideProps = async ({ query }) => { + await exchangeAuthCodeForToken( + query.code as string, + query.profileId as string + ); + return { props: {} }; +}; + + +// Frontend side of the page. Here we're only notifying parent tab (opener) that we can be closed now. +export default function WiseRedirectPage() { + + // runs on page load + useEffect(() => { + notifyOpener(MESSAGE_SUCCESS); + }, []); + + return

Nothing to see here..

; +} diff --git a/oauth-connect-popup/src/popupHandler.ts b/oauth-connect-popup/src/popupHandler.ts new file mode 100644 index 0000000..2ac49e3 --- /dev/null +++ b/oauth-connect-popup/src/popupHandler.ts @@ -0,0 +1,35 @@ +const MESSAGE_SUCCESS = 'wise-oauth-success'; + +// Opens Wise OAuth page in a popup window. Wrapped with a Promise so it would be easy to consume. +// After user "gives access", they get redirected (within popup) back to /pages/wise-redirect.tsx. +// This handler listens for a broadcasted "success" event (emitted by /pages/wise-redirect.tsx) +// and then closes the popup and marks the promise as resolved. +export function popupFlow(url: string) { + return new Promise((resolve) => { + openPopup(url, resolve); + }); +} + +function openPopup(url: string, resolve: (value?: unknown) => void) { + // Opens the page in popup window + const page = window.open( + url, + 'wise-oauth-connect-page', + `resizable,scrollbars,status,location,width=580,height=800,top=200,left=500` // not calculating screen center to keep it simple + ); + + const handleEvents = (event: MessageEvent) => { + if (event.data === MESSAGE_SUCCESS) { + closePopup(); + resolve(); + } + }; + + const closePopup = () => { + page?.close(); + window.removeEventListener('message', handleEvents); + }; + + // Start listening for events emitted by the page inside popup (eventually /pages/wise-redirect.tsx) + window.addEventListener('message', handleEvents); +} diff --git a/oauth-connect-popup/tsconfig.json b/oauth-connect-popup/tsconfig.json new file mode 100644 index 0000000..670224f --- /dev/null +++ b/oauth-connect-popup/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/oauth-connect/pages/index.tsx b/oauth-connect/pages/index.tsx index 08faa6d..22f3430 100644 --- a/oauth-connect/pages/index.tsx +++ b/oauth-connect/pages/index.tsx @@ -7,12 +7,13 @@ import { getWiseEnvironmentConfig, getSelectedWiseProfileId, isWiseTokenAboutToExpire, + getWiseOAuthPageURL, } from '../../common/db'; import { fetchProfileDetails } from '../../common/server/fetchProfileDetails'; import { refreshWiseToken } from '../../common/server/refreshWiseToken'; type PageProps = { - wiseOauthPageUrl?: string; + wiseOAuthPageURL?: string; redirectUri?: string; name?: string; }; @@ -41,8 +42,8 @@ export const getServerSideProps: GetServerSideProps = async () => { } // Accounts have not been connected. We render a "Connect your Wise account" button (see below) - const wiseOauthPageUrl = `${config.oauthPageUrl}?client_id=${config.clientId}&redirect_uri=${config.redirectUri}`; - return { props: { wiseOauthPageUrl, redirectUri: config.redirectUri } }; + const wiseOAuthPageURL = getWiseOAuthPageURL(); + return { props: { wiseOAuthPageURL, redirectUri: config.redirectUri } }; }; // Frontend component of the page. @@ -79,7 +80,7 @@ const OAuthConnectPage: FC = (props) => (

)} - + Connect your Wise account