From dbb60482fb0eef68257bcd8b6eac0e996ea6cdcb Mon Sep 17 00:00:00 2001 From: Josh Balfour Date: Mon, 13 Oct 2025 11:29:17 +0100 Subject: [PATCH 1/4] add org switcher and indicator --- .../src/core/__styles__/nav.ts | 31 +++++ .../src/core/connect-session.ts | 33 ++++- packages/developer-portal/src/core/menu.tsx | 117 +++++++++++++++++- 3 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 packages/developer-portal/src/core/__styles__/nav.ts diff --git a/packages/developer-portal/src/core/__styles__/nav.ts b/packages/developer-portal/src/core/__styles__/nav.ts new file mode 100644 index 0000000000..019e59822b --- /dev/null +++ b/packages/developer-portal/src/core/__styles__/nav.ts @@ -0,0 +1,31 @@ +import { css } from '@linaria/core' + +export const NavWithOrgPicker = css` + .el-nav-menu-option:first-child { + height: auto; + } + .el-nav-menu-option:first-child:hover { + cursor: default; + color: var(--color-black); + } + .el-nav-item:nth-last-of-type(2) { + margin-right: auto !important; + } + .el-nav-item:last-of-type { + margin-right: 0 !important; + background: none !important; + } + + @media screen and (min-width: 768px) { + .el-nav-item:last-of-type { + margin-right: 0 !important; + } + } + + .el-nav-menu-option:first-of-type { + padding: 0; + margin-top: -0.5rem; + margin-bottom: 0.5rem; + border-bottom: 1px solid var(--color-grey-light); + } +` diff --git a/packages/developer-portal/src/core/connect-session.ts b/packages/developer-portal/src/core/connect-session.ts index eb1e890bfd..ac3b3ca0e2 100644 --- a/packages/developer-portal/src/core/connect-session.ts +++ b/packages/developer-portal/src/core/connect-session.ts @@ -1,7 +1,38 @@ import { ReapitConnectBrowserSession } from '@reapit/connect-session' +import { v4 as uuid } from 'uuid' + +class ReapitConnectBrowserSessionExtended extends ReapitConnectBrowserSession { + constructor(config) { + super(config) + } + + public async changeOrg(chosenOrgId: string, chosenOrgType?: string, redirectUri?: string): Promise { + const authRedirectUri = redirectUri || this['connectLoginRedirectPath'] + const params = new URLSearchParams(window.location.search) + params.delete('code') + const search = params ? `?${params.toString()}` : '' + const internalRedirectPath = encodeURIComponent(`${window.location.pathname}${search}`) + const stateNonce = uuid() + this['refreshTokenStorage'].setItem(stateNonce, internalRedirectPath) + const code_challenge = await this['encryptCodeVerifier'](this['codeVerifier'](stateNonce)) + + const orgChoice = {} + if (chosenOrgId) { + orgChoice['chosenOrgId'] = chosenOrgId + } + if (chosenOrgType) { + orgChoice['chosenOrgType'] = chosenOrgType + } + + let location = `${this['connectOAuthUrl']}/authorize?response_type=code&client_id=${this['connectClientId']}&redirect_uri=${authRedirectUri}&state=${stateNonce}&${new URLSearchParams(orgChoice).toString()}` + if (this['usePKCE']) location += `&code_challenge_method=S256&code_challenge=${code_challenge}` + + window.location.href = location + } +} // Needs to be a singleton as the class is stateful -export const reapitConnectBrowserSession = new ReapitConnectBrowserSession({ +export const reapitConnectBrowserSession = new ReapitConnectBrowserSessionExtended({ connectClientId: process.env.connectClientId, connectOAuthUrl: process.env.connectOAuthUrl, connectLoginRedirectPath: '/apps', diff --git a/packages/developer-portal/src/core/menu.tsx b/packages/developer-portal/src/core/menu.tsx index c56a0353bb..98f7bb8ae3 100644 --- a/packages/developer-portal/src/core/menu.tsx +++ b/packages/developer-portal/src/core/menu.tsx @@ -1,7 +1,7 @@ -import React, { FC } from 'react' +import React, { FC, useEffect } from 'react' import { useNavigate, useLocation } from 'react-router' import Routes from '../constants/routes' -import { Icon, NavResponsive, NavResponsiveAvatarOption, NavResponsiveOption } from '@reapit/elements' +import { ElChipLabel, Icon, NavResponsive, NavResponsiveAvatarOption, NavResponsiveOption } from '@reapit/elements' import { memo } from 'react' import { navigateRoute, openNewPage } from '../utils/navigation' // Comment out after Christmas @@ -18,12 +18,117 @@ import { reapitConnectBrowserSession } from './connect-session' import { validate as isUuid } from 'uuid' import { getAvatarInitials } from '@reapit/utils-react' import { useGlobalState } from './use-global-state' +import { NavWithOrgPicker } from './__styles__/nav' +import { css } from '@linaria/core' const XmasImage = styled.img` height: 2.5rem; width: 2.5rem; ` +const CurrentOrgNameStyled = styled(ElChipLabel)` + margin-right: 0.5rem; +` + +const CurrentOrgName = () => { + const { connectSession } = useReapitConnect(reapitConnectBrowserSession) + const loginIdentity = selectLoginIdentity(connectSession) + const b = connectSession?.idToken.split('.')?.[1] + if (!b) { + return null + } + + try { + const orgTypes = JSON.parse(JSON.parse(atob(b)).organisationTypes) as { + id: string + name: string + types: ('customer' | 'developer' | 'organisation')[] + }[] + + if (orgTypes.map((o) => o.types).flat().length === 1) { + return null + } + } catch (e) { + console.error(e) + return null + } + + return {loginIdentity?.orgName} +} + +const ChooseableOrgType = css` + cursor: pointer; + + &:hover { + color: var(--intent-primary); + } +` + +const OrgPicker = () => { + const { connectSession } = useReapitConnect(reapitConnectBrowserSession) + const b = connectSession?.idToken.split('.')?.[1] + if (!b) { + return null + } + + const orgTypes = JSON.parse(JSON.parse(atob(b)).organisationTypes) as { + id: string + name: string + types: ('customer' | 'developer' | 'organisation')[] + }[] + + const switchOrg = (orgId: string, orgType?: string) => { + reapitConnectBrowserSession.changeOrg(orgId, orgType) + } + + return ( +
+

Switch Organisation

+
    + {orgTypes.map((ot) => { + const multiType = ot.types.length > 1 + return ( +
  • { + if (!multiType) { + switchOrg(ot.id, ot.types[0]) + } + }} + > + {ot.name} + {multiType ? ( +
      + {ot.types.map((t) => ( +
    • { + switchOrg(ot.id, t) + }} + > + {t} +
    • + ))} +
    + ) : null} +
  • + ) + })} +
+
+ ) +} + dayjs.extend(isBetween) export const XmasLogo: React.FC = () => { @@ -211,10 +316,15 @@ export const Menu: FC = () => { }, ], }, + { + itemIndex: 23, + text: , + }, ].filter(Boolean) as NavResponsiveOption[] return ( { avatarText={getAvatarInitials(connectSession)} avatarOptions={ [ + { + text: , + }, { callback: navigateRoute(navigate, Routes.SETTINGS_PROFILE), text: 'Profile', From b47edf360abe7b423cddf3b6aac26144150c9b04 Mon Sep 17 00:00:00 2001 From: Josh Balfour Date: Tue, 14 Oct 2025 12:34:28 +0100 Subject: [PATCH 2/4] tidy --- packages/developer-portal/src/core/menu.tsx | 39 ++++++++++----------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/developer-portal/src/core/menu.tsx b/packages/developer-portal/src/core/menu.tsx index 98f7bb8ae3..5e714d05ba 100644 --- a/packages/developer-portal/src/core/menu.tsx +++ b/packages/developer-portal/src/core/menu.tsx @@ -1,4 +1,4 @@ -import React, { FC, useEffect } from 'react' +import React, { FC } from 'react' import { useNavigate, useLocation } from 'react-router' import Routes from '../constants/routes' import { ElChipLabel, Icon, NavResponsive, NavResponsiveAvatarOption, NavResponsiveOption } from '@reapit/elements' @@ -30,9 +30,8 @@ const CurrentOrgNameStyled = styled(ElChipLabel)` margin-right: 0.5rem; ` -const CurrentOrgName = () => { +const useOrgTypes = () => { const { connectSession } = useReapitConnect(reapitConnectBrowserSession) - const loginIdentity = selectLoginIdentity(connectSession) const b = connectSession?.idToken.split('.')?.[1] if (!b) { return null @@ -45,11 +44,18 @@ const CurrentOrgName = () => { types: ('customer' | 'developer' | 'organisation')[] }[] - if (orgTypes.map((o) => o.types).flat().length === 1) { - return null - } - } catch (e) { - console.error(e) + return orgTypes + } catch { + return null + } +} + +const CurrentOrgName = () => { + const orgTypes = useOrgTypes() + const { connectSession } = useReapitConnect(reapitConnectBrowserSession) + const loginIdentity = selectLoginIdentity(connectSession) + + if (orgTypes?.map((o) => o.types).flat().length === 1) { return null } @@ -65,26 +71,19 @@ const ChooseableOrgType = css` ` const OrgPicker = () => { - const { connectSession } = useReapitConnect(reapitConnectBrowserSession) - const b = connectSession?.idToken.split('.')?.[1] - if (!b) { - return null - } - - const orgTypes = JSON.parse(JSON.parse(atob(b)).organisationTypes) as { - id: string - name: string - types: ('customer' | 'developer' | 'organisation')[] - }[] + const orgTypes = useOrgTypes() const switchOrg = (orgId: string, orgType?: string) => { reapitConnectBrowserSession.changeOrg(orgId, orgType) } + if (!orgTypes || orgTypes.length <= 1) { + return null + } + return (
Date: Tue, 14 Oct 2025 12:44:58 +0100 Subject: [PATCH 3/4] b64url --- packages/developer-portal/src/core/menu.tsx | 27 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/developer-portal/src/core/menu.tsx b/packages/developer-portal/src/core/menu.tsx index 5e714d05ba..52364f0963 100644 --- a/packages/developer-portal/src/core/menu.tsx +++ b/packages/developer-portal/src/core/menu.tsx @@ -30,22 +30,41 @@ const CurrentOrgNameStyled = styled(ElChipLabel)` margin-right: 0.5rem; ` +function parseJwt(token: string) { + const base64Url = token.split('.')[1] + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') + const jsonPayload = decodeURIComponent( + window + .atob(base64) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + }) + .join(''), + ) + + return JSON.parse(jsonPayload) +} + const useOrgTypes = () => { const { connectSession } = useReapitConnect(reapitConnectBrowserSession) - const b = connectSession?.idToken.split('.')?.[1] + const b = connectSession?.idToken && parseJwt(connectSession?.idToken) if (!b) { return null } + console.log(b) + try { - const orgTypes = JSON.parse(JSON.parse(atob(b)).organisationTypes) as { + const orgTypes = JSON.parse(b.organisationTypes) as { id: string name: string types: ('customer' | 'developer' | 'organisation')[] }[] return orgTypes - } catch { + } catch (e) { + console.error(e) return null } } @@ -77,6 +96,8 @@ const OrgPicker = () => { reapitConnectBrowserSession.changeOrg(orgId, orgType) } + console.log(orgTypes) + if (!orgTypes || orgTypes.length <= 1) { return null } From e7fe1bffa12508c909d25e98900ee5c3fba3af34 Mon Sep 17 00:00:00 2001 From: Josh Balfour Date: Tue, 14 Oct 2025 12:49:23 +0100 Subject: [PATCH 4/4] clean --- packages/developer-portal/src/core/menu.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/developer-portal/src/core/menu.tsx b/packages/developer-portal/src/core/menu.tsx index 52364f0963..c6cb886e3b 100644 --- a/packages/developer-portal/src/core/menu.tsx +++ b/packages/developer-portal/src/core/menu.tsx @@ -53,8 +53,6 @@ const useOrgTypes = () => { return null } - console.log(b) - try { const orgTypes = JSON.parse(b.organisationTypes) as { id: string @@ -63,8 +61,7 @@ const useOrgTypes = () => { }[] return orgTypes - } catch (e) { - console.error(e) + } catch { return null } } @@ -96,8 +93,6 @@ const OrgPicker = () => { reapitConnectBrowserSession.changeOrg(orgId, orgType) } - console.log(orgTypes) - if (!orgTypes || orgTypes.length <= 1) { return null }