diff --git a/apps/customer-portal/microapp/.env.example b/apps/customer-portal/microapp/.env.example new file mode 100644 index 000000000..1209baadb --- /dev/null +++ b/apps/customer-portal/microapp/.env.example @@ -0,0 +1 @@ +VITE_BACKEND_URL=https://example.com/api \ No newline at end of file diff --git a/apps/customer-portal/microapp/.gitignore b/apps/customer-portal/microapp/.gitignore new file mode 100644 index 000000000..45349c6b6 --- /dev/null +++ b/apps/customer-portal/microapp/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +package-lock.json + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# config.js +/public/config.js + +.env \ No newline at end of file diff --git a/apps/customer-portal/microapp/.prettierrc b/apps/customer-portal/microapp/.prettierrc new file mode 100644 index 000000000..6aa4deb48 --- /dev/null +++ b/apps/customer-portal/microapp/.prettierrc @@ -0,0 +1,8 @@ +{ + "endOfLine": "lf", + "singleQuote": false, + "trailingComma": "all", + "printWidth": 120, + "semi": true, + "tabWidth": 2 +} diff --git a/apps/customer-portal/microapp/README.md b/apps/customer-portal/microapp/README.md new file mode 100644 index 000000000..a78d9031d --- /dev/null +++ b/apps/customer-portal/microapp/README.md @@ -0,0 +1,15 @@ +## Setting Up Environment Configuration + +1. **Create a file named `config.js` inside the `public` folder in root directory.** +2. **Add the following code to `config.js`:** + + ```javascript + window.config = { + ASGARDEO_BASE_URL: "", // Asgardeo base URL + CLIENT_ID: "", // Asgardeo client ID + SIGN_IN_REDIRECT_URL: "", // Redirect URL after sign in + SIGN_OUT_REDIRECT_URL: "", // Redirect URL after sign out + BACKEND_BASE_URL: "", // Backend API base URL + IS_MICROAPP: true, + }; + ``` diff --git a/apps/customer-portal/microapp/eslint.config.js b/apps/customer-portal/microapp/eslint.config.js new file mode 100644 index 000000000..0386a6708 --- /dev/null +++ b/apps/customer-portal/microapp/eslint.config.js @@ -0,0 +1,46 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; +import prettier from "eslint-plugin-prettier"; +import prettierConfig from "eslint-config-prettier"; + +export default defineConfig([ + globalIgnores(["dist"]), + { + files: ["**/*.{ts,tsx}"], + extends: [js.configs.recommended, ...tseslint.configs.recommended, prettierConfig], + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + prettier: prettier, + }, + rules: { + ...reactHooks.configs.recommended.rules, + ...reactRefresh.configs.vite.rules, + "prettier/prettier": "error", + }, + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]); diff --git a/apps/customer-portal/microapp/index.html b/apps/customer-portal/microapp/index.html new file mode 100644 index 000000000..87032d99a --- /dev/null +++ b/apps/customer-portal/microapp/index.html @@ -0,0 +1,32 @@ + + + + + + + + + Vite + React + TS + + +
+ + + + diff --git a/apps/customer-portal/microapp/package.json b/apps/customer-portal/microapp/package.json new file mode 100644 index 000000000..c2f1b8413 --- /dev/null +++ b/apps/customer-portal/microapp/package.json @@ -0,0 +1,53 @@ +{ + "name": "microapp", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "prettier": "prettier --write .", + "prettier:check": "prettier --check ." + }, + "dependencies": { + "@asgardeo/auth-react": "^5.3.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@headlessui/react": "^2.2.3", + "@mui/icons-material": "^7.3.6", + "@mui/material": "^7.3.6", + "@tanstack/react-query": "^5.90.12", + "@types/react": "^19.1.3", + "@types/react-dom": "^19.1.3", + "@types/react-router-dom": "^5.3.3", + "axios": "^1.13.2", + "jwt-decode": "^4.0.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.9.1", + "vite-plugin-svgr": "^4.5.0", + "vite-tsconfig-paths": "^5.1.4", + "zustand": "^5.0.9" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tanstack/eslint-plugin-query": "^5.91.2", + "@types/node": "^24.10.1", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.3", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.5.0", + "prettier": "3.6.2", + "typescript": "~5.8.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.1.7" + } +} diff --git a/apps/customer-portal/microapp/public/wso2-logo.svg b/apps/customer-portal/microapp/public/wso2-logo.svg new file mode 100644 index 000000000..a85f74c35 --- /dev/null +++ b/apps/customer-portal/microapp/public/wso2-logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/customer-portal/microapp/src/App.css b/apps/customer-portal/microapp/src/App.css new file mode 100644 index 000000000..df71b0b64 --- /dev/null +++ b/apps/customer-portal/microapp/src/App.css @@ -0,0 +1,56 @@ +/* +Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). + +WSO2 LLC. licenses this file to you under the Apache License, +Version 2.0 (the "License"); you may not use this file except +in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +body { + margin: 0; + font-family: "Arial", sans-serif; + background-color: #fff; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background-color: #fff; + padding: 0 16px; +} + +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +.spinner { + border: 4px solid rgba(0, 0, 0, 0.1); + border-left-color: #d25d23; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/apps/customer-portal/microapp/src/App.tsx b/apps/customer-portal/microapp/src/App.tsx new file mode 100644 index 000000000..af3a6f54b --- /dev/null +++ b/apps/customer-portal/microapp/src/App.tsx @@ -0,0 +1,31 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import React from "react"; +import HomePage from "@pages/HomePage"; +import { HashRouter as Router, Route, Routes } from "react-router-dom"; + +const App: React.FC = () => { + return ( + + + } /> + + + ); +}; + +export default App; diff --git a/apps/customer-portal/microapp/src/components/microapp-bridge/index.ts b/apps/customer-portal/microapp/src/components/microapp-bridge/index.ts new file mode 100644 index 000000000..ad1bab9c1 --- /dev/null +++ b/apps/customer-portal/microapp/src/components/microapp-bridge/index.ts @@ -0,0 +1,236 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { ErrorMessages } from "@utils/constants"; +import { Topic } from "./types"; +import type { LogLevel, TopicType } from "./types"; + +type Callback = (data?: T) => void; + +// Bridge event topics used for communication between the main app and micro apps. +const TOPIC = { + TOKEN: "token", + QR_REQUEST: "qr_request", + SAVE_LOCAL_DATA: "save_local_data", + GET_LOCAL_DATA: "get_local_data", + ALERT: "alert", + CONFIRM_ALERT: "confirm_alert", + TOTP: "totp", +}; + +declare global { + interface Window { + nativebridge?: { + requestToken: () => void; + resolveToken: (token: string) => void; + requestQR: () => void; + resolveQR: (qrString: string) => void; + requestItemList: () => void; + resolveConfirmAlert: (action: string) => void; + resolveQRCode: (qrData: string) => void; + rejectQRCode: (error: string) => void; + resolveSaveLocalData: () => void; + rejectSaveLocalData: (error: string) => void; + resolveGetLocalData: (encodedData: { value?: string }) => void; + rejectGetLocalData: (error: string) => void; + resolveTotpQrMigrationData: (encodedData: { data: string }) => void; + rejectTotpQrMigrationData: (error: string) => void; + }; + ReactNativeWebView?: { + postMessage: (message: string) => void; + }; + } +} + +// Function to get token from React Native +export const getToken = (callback: Callback): void => { + if (window.nativebridge) { + window.nativebridge.requestToken(); + window.nativebridge.resolveToken = (token: string) => { + callback(token); + }; + } else { + console.error("Native bridge is not available"); + callback(); + } +}; + +// Function to show alert in React Native +export const showAlert = (title: string, message: string, buttonText: string): void => { + if (window.nativebridge && window.ReactNativeWebView) { + const alertData = JSON.stringify({ + topic: TOPIC.ALERT, + data: { title, message, buttonText }, + }); + + window.ReactNativeWebView.postMessage(alertData); + } else { + console.error("Native bridge is not available"); + } +}; + +// Function to show confirm alert in React Native +export const showConfirmAlert = ( + title: string, + message: string, + confirmButtonText: string, + cancelButtonText: string, + confirmCallback: () => void, + cancelCallback: () => void, +): void => { + if (window.nativebridge && window.ReactNativeWebView) { + const confirmData = JSON.stringify({ + topic: TOPIC.CONFIRM_ALERT, + data: { title, message, confirmButtonText, cancelButtonText }, + }); + + window.ReactNativeWebView.postMessage(confirmData); + + // Handling response from React Native side + window.nativebridge.resolveConfirmAlert = (action: string) => { + if (action === "confirm") { + confirmCallback(); + } else if (action === "cancel") { + cancelCallback(); + } + }; + } else { + console.error("Native bridge is not available"); + } +}; + +// Scan QR Code +export const scanQRCode = ( + successCallback: (qrData: string) => void, + failedToRespondCallback: (error: string) => void, +): void => { + if (window.nativebridge && window.ReactNativeWebView) { + window.ReactNativeWebView.postMessage(JSON.stringify({ topic: TOPIC.QR_REQUEST })); + + window.nativebridge.resolveQRCode = (qrData: string) => successCallback(qrData); + window.nativebridge.rejectQRCode = (error: string) => failedToRespondCallback(error); + } else { + console.error("Native bridge is not available"); + } +}; + +// Save Local Data +export const saveLocalData = ( + key: string, + value: unknown, + callback: () => void, + failedToRespondCallback: (error: string) => void, +): void => { + key = key.toString().replace(" ", "-").toLowerCase(); + const encodedValue = btoa(JSON.stringify(value)); + + if (window.nativebridge && window.ReactNativeWebView) { + window.ReactNativeWebView.postMessage( + JSON.stringify({ + topic: TOPIC.SAVE_LOCAL_DATA, + data: { key, value: encodedValue }, + }), + ); + + window.nativebridge.resolveSaveLocalData = callback; + window.nativebridge.rejectSaveLocalData = (error: string) => failedToRespondCallback(error); + } else { + console.error("Native bridge is not available"); + } +}; + +// Get Local Data +export const getLocalData = ( + key: string, + callback: (data: T | null) => void, + failedToRespondCallback: (error: string) => void, +): void => { + key = key.toString().replace(" ", "-").toLowerCase(); + + if (window.nativebridge && window.ReactNativeWebView) { + window.ReactNativeWebView.postMessage(JSON.stringify({ topic: TOPIC.GET_LOCAL_DATA, data: { key } })); + + window.nativebridge.resolveGetLocalData = (encodedData: { value?: string }) => { + if (!encodedData.value) { + callback(null); + } else { + callback(JSON.parse(atob(encodedData.value)) as T); + } + }; + + window.nativebridge.rejectGetLocalData = (error: string) => failedToRespondCallback(error); + } else { + console.error("Native bridge is not available"); + } +}; + +// TOTP QR Migration Data +export const totpQrMigrationData = ( + callback: (data: string[]) => void, + failedToRespondCallback: (error: string) => void, +): void => { + if (window.nativebridge && window.ReactNativeWebView) { + window.ReactNativeWebView.postMessage(JSON.stringify({ topic: TOPIC.TOTP })); + + window.nativebridge.resolveTotpQrMigrationData = (encodedData: { data: string }) => { + if (encodedData.data) { + callback(encodedData.data.replace(" ", "").split(",")); + } else { + callback([]); + } + }; + + window.nativebridge.rejectTotpQrMigrationData = (error: string) => failedToRespondCallback(error); + } else { + console.error("Native bridge is not available"); + } +}; + +/** + * Trigger an action in the super app + * @param topic - The topic to trigger + * @param data - The data to send + */ +const triggerSuperAppAction = (topic: TopicType, data?: unknown): void => { + if (window.ReactNativeWebView) { + const messageData = JSON.stringify({ + topic, + data, + }); + window.ReactNativeWebView.postMessage(messageData); + } else { + console.error(ErrorMessages.NATIVE_BRIDGE_NOT_AVAILABLE); + } +}; + +/** + * Send a log message to the native side + * @param message - The message to send + * @param data - The data to send + * @param level - The level of the log + */ +export const sendNativeLog = (message?: string, data?: unknown, level: LogLevel = "debug"): void => { + if (window.nativebridge && window.ReactNativeWebView) { + triggerSuperAppAction(Topic.nativeLog, { + message, + data, + level, + }); + } else { + // TODO: Replace this with Logger.error(ErrorMessages.NATIVE_BRIDGE_NOT_AVAILABLE) + console.error(ErrorMessages.NATIVE_BRIDGE_NOT_AVAILABLE); + } +}; diff --git a/apps/customer-portal/microapp/src/components/microapp-bridge/types.ts b/apps/customer-portal/microapp/src/components/microapp-bridge/types.ts new file mode 100644 index 000000000..58baebdba --- /dev/null +++ b/apps/customer-portal/microapp/src/components/microapp-bridge/types.ts @@ -0,0 +1,34 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +export const Topic = { + token: "token", + nativeLog: "native_log", + navigateToMyApps: "close_webview", + saveLocalData: "save_local_data", + getLocalData: "get_local_data", + deviceSafeAreaInsets: "device_safe_area_insets", + deleteLocalData: "delete_local_data", + openUrl: "open_url", + scheduleLocalNotification: "scheduling_local_notification", + cancelLocalNotification: "cancelling_local_notification", + clearAllLocalNotifications: "clearing_all_local_notifications", + qrRequest: "qr_request", +} as const; + +export type TopicType = (typeof Topic)[keyof typeof Topic]; + +export type LogLevel = "error" | "warn" | "info" | "debug"; diff --git a/apps/customer-portal/microapp/src/config/config.ts b/apps/customer-portal/microapp/src/config/config.ts new file mode 100644 index 000000000..7e3aacfc4 --- /dev/null +++ b/apps/customer-portal/microapp/src/config/config.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +declare global { + interface Window { + config: { + CLIENT_ID: string; + SIGN_IN_REDIRECT_URL: string; + SIGN_OUT_REDIRECT_URL: string; + ASGARDEO_BASE_URL: string; + IS_MICROAPP: boolean; + BACKEND_BASE_URL: string; + }; + } +} + +export const CLIENT_ID = window.config.CLIENT_ID; +export const SIGN_IN_REDIRECT_URL = window.config.SIGN_IN_REDIRECT_URL; +export const SIGN_OUT_REDIRECT_URL = window.config.SIGN_OUT_REDIRECT_URL; +export const ASGARDEO_BASE_URL = window.config.ASGARDEO_BASE_URL; +export const IS_MICROAPP = window.config.IS_MICROAPP; + +// TODO: Uncomment and update the `baseUrl` variable when the backend URL configuration is available and in use. +// const baseUrl = window.config.BACKEND_BASE_URL; + +export const serviceUrls = { + serviceUrls: { + // TODO: Add service URLs here as needed for future implementation. + }, +}; diff --git a/apps/customer-portal/microapp/src/config/constants.ts b/apps/customer-portal/microapp/src/config/constants.ts new file mode 100644 index 000000000..6a07eca6d --- /dev/null +++ b/apps/customer-portal/microapp/src/config/constants.ts @@ -0,0 +1,51 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { + AutorenewOutlined, + Bedtime, + CalendarMonthOutlined, + ChatBubbleOutline, + Cloud, + ErrorOutline, + PeopleAltOutlined, + Report, + SettingsOutlined, + ThumbUpAlt, + type SvgIconComponent, +} from "@mui/icons-material"; +import type { ProjectMetricKey, ProjectMetricMeta, ProjectStatus, ProjectType } from "@features/projects"; + +export const INPUT_INVALID_MSG_GATEWAY = "INPUT_INVALID_MSG_GATEWAY"; + +export const PROJECT_METRIC_META: Record = { + cases: { label: "Cases:", color: "semantic.portal.accent.orange", icon: ErrorOutline }, + chats: { label: "Chats:", color: "semantic.portal.accent.blue", icon: ChatBubbleOutline }, + service: { label: "Service:", color: "semantic.portal.accent.purple", icon: SettingsOutlined }, + change: { label: "Change:", color: "semantic.portal.accent.cyan", icon: AutorenewOutlined }, + users: { label: "Users:", color: "text.primary", icon: PeopleAltOutlined }, + date: { label: "Date:", icon: CalendarMonthOutlined }, +}; + +export const PROJECT_TYPE_META: Record = { + Regular: { icon: Bedtime }, + "Managed Cloud": { icon: Cloud }, +}; + +export const PROJECT_STATUS_META: Record = { + "All Good": { color: "success", icon: ThumbUpAlt }, + "Needs Attention": { color: "warning", icon: Report }, +}; diff --git a/apps/customer-portal/microapp/src/config/endpoints.ts b/apps/customer-portal/microapp/src/config/endpoints.ts new file mode 100644 index 000000000..e8c023cbc --- /dev/null +++ b/apps/customer-portal/microapp/src/config/endpoints.ts @@ -0,0 +1,23 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL; + +if (!BACKEND_URL) { + throw new Error("VITE_BACKEND_URL is not defined"); +} + +export const PROJECTS_ENDPOINT = "/x_wso2_customer_0/projects"; diff --git a/apps/customer-portal/microapp/src/features/projects/ProjectCard.tsx b/apps/customer-portal/microapp/src/features/projects/ProjectCard.tsx new file mode 100644 index 000000000..665a250c3 --- /dev/null +++ b/apps/customer-portal/microapp/src/features/projects/ProjectCard.tsx @@ -0,0 +1,104 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { ArrowForward, type SvgIconComponent } from "@mui/icons-material"; +import { Box, ButtonBase as Button, Card, Chip, Grid, Stack, Typography } from "@mui/material"; +import { PROJECT_METRIC_META, PROJECT_STATUS_META, PROJECT_TYPE_META } from "@root/src/config/constants"; + +export type ProjectStatus = "All Good" | "Needs Attention"; +export type ProjectType = "Managed Cloud" | "Regular"; +export type ProjectMetricKey = "cases" | "chats" | "service" | "change" | "users" | "date"; +export type ProjectMetricValue = number | string; +export type ProjectMetrics = Partial>; + +export interface ProjectMetricMeta { + label: string; + icon: SvgIconComponent; + color?: string; +} + +export interface ProjectCardProps { + id: string; + name: string; + description: string; + type: ProjectType; + status: ProjectStatus; + metrics: ProjectMetrics; +} + +export function ProjectCard({ id, name, description, type, status, metrics }: ProjectCardProps) { + const TypeChipIcon = PROJECT_TYPE_META[type].icon; + const StatusChipIcon = PROJECT_STATUS_META[status].icon; + const statusChipColorVariant = PROJECT_STATUS_META[status].color; + + return ( + ({ borderRadius: 3, border: `1px solid ${theme.palette.divider}` })}> + + + + {id} + + } + iconPosition="end" + /> + + + {name} + + } sx={{ alignSelf: "start", borderRadius: 1 }} /> + {description} + + + {Object.keys(metrics).map((key) => { + const meta = PROJECT_METRIC_META[key as ProjectMetricKey]; + const value = metrics[key as ProjectMetricKey]; + + if (value === undefined) return null; + + return ( + + + + ); + })} + + + + + + ); +} + +function MetricItem({ meta, value }: { meta: ProjectMetricMeta; value: ProjectMetricValue }) { + return ( + + ({ color: "text.secondary", fontSize: theme.typography.pxToRem(20) })} /> + + {meta.label} + + + {value} + + + ); +} diff --git a/apps/customer-portal/microapp/src/features/projects/index.ts b/apps/customer-portal/microapp/src/features/projects/index.ts new file mode 100644 index 000000000..b0ecb3aa4 --- /dev/null +++ b/apps/customer-portal/microapp/src/features/projects/index.ts @@ -0,0 +1,17 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +export * from "./ProjectCard"; diff --git a/apps/customer-portal/microapp/src/icons/.gitkeep b/apps/customer-portal/microapp/src/icons/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/customer-portal/microapp/src/index.css b/apps/customer-portal/microapp/src/index.css new file mode 100644 index 000000000..b70b3de2d --- /dev/null +++ b/apps/customer-portal/microapp/src/index.css @@ -0,0 +1,28 @@ +/* +Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). + +WSO2 LLC. licenses this file to you under the Apache License, +Version 2.0 (the "License"); you may not use this file except +in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"); + +* { + font-family: "Plus Jakarta Sans", sans-serif; + font-optical-sizing: auto; +} + +body { + margin: 0; +} diff --git a/apps/customer-portal/microapp/src/main.tsx b/apps/customer-portal/microapp/src/main.tsx new file mode 100644 index 000000000..c40a8919d --- /dev/null +++ b/apps/customer-portal/microapp/src/main.tsx @@ -0,0 +1,48 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "@src/index.css"; +import App from "@src/App"; +import { AuthProvider } from "@asgardeo/auth-react"; +import { ASGARDEO_BASE_URL, CLIENT_ID, SIGN_IN_REDIRECT_URL, SIGN_OUT_REDIRECT_URL } from "@config/config"; +import { CssBaseline, ThemeProvider } from "@mui/material"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import theme from "@src/theme"; + +const authConfig = { + clientID: CLIENT_ID || "", + baseUrl: ASGARDEO_BASE_URL || "", + signInRedirectURL: SIGN_IN_REDIRECT_URL || "", + signOutRedirectURL: SIGN_OUT_REDIRECT_URL || "", + scope: ["openid", "profile", "email"], +}; + +const queryClient = new QueryClient(); + +createRoot(document.getElementById("root")!).render( + + + + + + + + + + , +); diff --git a/apps/customer-portal/microapp/src/pages/HomePage.tsx b/apps/customer-portal/microapp/src/pages/HomePage.tsx new file mode 100644 index 000000000..0b2d14404 --- /dev/null +++ b/apps/customer-portal/microapp/src/pages/HomePage.tsx @@ -0,0 +1,75 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { Box, CircularProgress, Stack, Typography } from "@mui/material"; +import { FolderOpen } from "@mui/icons-material"; +import { ProjectCard } from "@features/projects"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { getProjects } from "@src/services/projects"; +import { Suspense } from "react"; + +export default function HomePage() { + return ( + + + + } + > + + + ); +} + +function HomeContent() { + const { data } = useSuspenseQuery({ + queryKey: ["projects"], + queryFn: getProjects, + }); + + return ( + + + + + Select Your Project + + + + Choose a project to access your support cases, chat history, and dashboard + + + {data.map((project) => ( + + ))} + + + + Need access to another project? Contact your administrator + + + + ); +} diff --git a/apps/customer-portal/microapp/src/services/apiClient.ts b/apps/customer-portal/microapp/src/services/apiClient.ts new file mode 100644 index 000000000..ebd88242f --- /dev/null +++ b/apps/customer-portal/microapp/src/services/apiClient.ts @@ -0,0 +1,166 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import axios, { type InternalAxiosRequestConfig } from "axios"; +import { Logger } from "@utils/logger"; +import { refreshToken } from "./auth"; +import { BACKEND_URL } from "@config/endpoints"; + +// Variables/constants +let isRefreshing = false; +let failedQueue: { + resolve: (value: unknown) => void; + reject: (reason?: unknown) => void; +}[] = []; + +// Holds the refresh token promise +let refreshTokenPromise: Promise | null = null; + +// axios instance +const apiClient = axios.create({ + baseURL: BACKEND_URL, +}); + +/** + * Request Interceptor + */ +apiClient.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + // Log the outgoing request + Logger.info(`Making ${config.method?.toUpperCase()} request to: ${config.baseURL || ""}${config.url || ""}`, { + url: config.url, + method: config.method, + baseURL: config.baseURL, + fullURL: `${config.baseURL || ""}${config.url || ""}`, + headers: config.headers, + }); + + // Use a singleton promise for token refresh + if (!refreshTokenPromise) { + refreshTokenPromise = refreshToken().finally(() => { + // Reset the promise once it's resolved or rejected + refreshTokenPromise = null; + }); + } + + try { + const token = ""; // TODO: Replace with `const token = await refreshTokenPromise;` + if (token) { + config.headers.Authorization = `Bearer ${token}`; + config.headers["Content-Type"] = "application/json"; + Logger.info("Added authorization token to request"); + } else { + Logger.warn("No token available for request"); + } + } catch (error) { + Logger.error("Failed to get token for request", error); + return Promise.reject(error); + } + + return config; + }, + (error) => { + Logger.error("Request interceptor error", error); + return Promise.reject(error); + }, +); + +/** + * Response Interceptor + */ +const processQueue = (error: unknown, token: string | null = null) => { + failedQueue.forEach((prom) => { + if (error) { + prom.reject(error); + } else { + prom.resolve(token); + } + }); + + failedQueue = []; +}; + +apiClient.interceptors.response.use( + (response) => { + // Any status code within the range of 2xx causes this function to trigger + Logger.info(`Successful response from ${response.config.method?.toUpperCase()} ${response.config.url}`, { + status: response.status, + statusText: response.statusText, + url: response.config.url, + data: { + ...response.data, + dataSize: response.data ? JSON.stringify(response.data).length : 0, + }, + }); + return response; + }, + async (error) => { + if (error.code === axios.isCancel(error)) { + return Promise.reject(error); + } + Logger.error(`API request failed`, { + status: error.response?.status, + statusText: error.response?.statusText, + url: error.config?.url, + method: error.config?.method, + message: error.message, + }); + + const originalRequest = error.config; + + if (error.response?.status === 401 && !originalRequest._retry) { + Logger.warn("Received 401 unauthorized, attempting token refresh"); + + if (isRefreshing) { + Logger.info("Token refresh already in progress, queuing request"); + return new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }); + }) + .then((token) => { + originalRequest.headers["Authorization"] = "Bearer " + token; + return apiClient(originalRequest); + }) + .catch((err) => { + return Promise.reject(err); + }); + } + + originalRequest._retry = true; + isRefreshing = true; + + try { + Logger.info("Attempting to refresh access token"); + const newAccessToken = await refreshToken(); + apiClient.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken}`; + originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; + processQueue(null, newAccessToken); + Logger.info("Token refresh successful, retrying original request"); + return apiClient(originalRequest); + } catch (refreshError) { + Logger.error("Token refresh failed", refreshError); + processQueue(refreshError, null); + console.error("Token refresh failed:", refreshError); + return Promise.reject(refreshError); + } finally { + isRefreshing = false; + } + } + + return Promise.reject(error); + }, +); + +export default apiClient; diff --git a/apps/customer-portal/microapp/src/services/auth.ts b/apps/customer-portal/microapp/src/services/auth.ts new file mode 100644 index 000000000..96d535b98 --- /dev/null +++ b/apps/customer-portal/microapp/src/services/auth.ts @@ -0,0 +1,138 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { jwtDecode } from "jwt-decode"; +import { getToken } from "@components/microapp-bridge"; +import { LocalStorageKeys } from "@utils/constants"; +import { Logger } from "@utils/logger"; +import { useUserStore, type User } from "../store/user"; + +// Token Payload +interface TokenPayload { + email?: string; + name?: string; + groups?: string[]; + given_name: string; + family_name: string; +} + +// These would be your actual token storage functions +export const getAccessToken = (): string | null => localStorage.getItem(LocalStorageKeys.accessToken); +export const setAccessToken = (token: string): void => localStorage.setItem(LocalStorageKeys.accessToken, token); +export const getIdToken = (): string | null => localStorage.getItem(LocalStorageKeys.idToken); +export const setIdToken = (token: string): void => localStorage.setItem(LocalStorageKeys.idToken, token); + +/** + * A function to refresh the token. + * This is a simplified version of the logic in the original `handleRequestWithNewToken`. + * It fetches a new token and updates it in storage. + */ +export const refreshToken = (): Promise => { + return new Promise((resolve, reject) => { + getToken((newIdToken: string | undefined) => { + if (newIdToken) { + setIdToken(newIdToken); + setAccessToken(newIdToken); + + // Automatically decode and store user information when token is refreshed + try { + initializeUserFromToken(); + Logger.info("User information updated after token refresh"); + } catch (error) { + Logger.warn("Failed to update user information after token refresh", error); + } + + resolve(newIdToken); + } else { + Logger.error("Failed to refresh token"); + reject("Failed to refresh token"); + } + }); + }); +}; + +/** + * Checks if the user belongs to a given group or groups based on the ID token. + * This is a direct replacement for `handleCheckGroups`. + * @param groupNames - A single group name or an array of group names. + * @returns boolean - True if the user is in at least one of the required groups. + */ +export const checkUserGroups = (groupNames: string | string[]): boolean => { + const token = getIdToken(); + + if (!token) { + Logger.error("ID token not found for group check."); + return false; + } + + const decoded = jwtDecode(token); + const userGroups = decoded.groups ?? []; + const requiredGroups = Array.isArray(groupNames) ? groupNames : [groupNames]; + + return requiredGroups.some((group) => userGroups.includes(group)); +}; + +/** + * Decodes the ID token and extracts user information. + * Stores the user information in the Zustand user store. + * @returns User object or null if token is invalid + */ +export const decodeTokenAndStoreUser = (): User | null => { + try { + const token = getIdToken(); + + if (!token) { + Logger.error("ID token not found for user decoding."); + useUserStore.getState().clearUser(); + return null; + } + + const decoded = jwtDecode(token); + Logger.info("Token decoded successfully", { + email: decoded.email, + name: decoded.name, + }); + + // Extract user information from token + const user: User = { + email: decoded.email || "", + name: `${decoded.given_name || ""} ${decoded.family_name || ""}`, + }; + + return user; + } catch (error) { + Logger.error("Failed to decode token and store user information", error); + useUserStore.getState().clearUser(); + return null; + } +}; + +/** + * Initializes user data from stored token. + * Should be called when the app starts or when a new token is received. + */ +export const initializeUserFromToken = (): void => { + useUserStore.getState().setLoading(true); + + try { + decodeTokenAndStoreUser(); + } catch (error) { + Logger.error("Failed to initialize user from token", error); + useUserStore.getState().clearUser(); + } finally { + useUserStore.getState().setLoading(false); + } +}; diff --git a/apps/customer-portal/microapp/src/services/projects.ts b/apps/customer-portal/microapp/src/services/projects.ts new file mode 100644 index 000000000..68ccb5240 --- /dev/null +++ b/apps/customer-portal/microapp/src/services/projects.ts @@ -0,0 +1,58 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import apiClient from "@src/services/apiClient"; +import type { ProjectCardProps } from "@features/projects"; +import { PROJECTS_ENDPOINT } from "@config/endpoints"; + +export interface ProjectsResponseType { + projects: [ + { + sysId: string; + name: string; + description: string; + projectKey: string; + createdOn: string; + activeChatsCount: number; + openCasesCount: number; + }, + ]; + pagination: { offset: number; limit: number; totalRecords: number }; +} + +export const getProjects = async (): Promise => { + const response = await apiClient.get(PROJECTS_ENDPOINT); + + return response.data.projects.map((project) => ({ + id: project.projectKey, + name: project.name, + description: project.description, + + // TODO: determine project type from backend + // Fallback to "Managed Cloud" until backend provides explicit field + type: "Managed Cloud", + + // TODO: determine project status from backend + // Fallback to "All Good" until backend provides explicit field + status: "All Good", + + metrics: { + cases: project.openCasesCount, + chats: project.activeChatsCount, + // TODO: populate remaining metrics when supported by backend + }, + })); +}; diff --git a/apps/customer-portal/microapp/src/store/user.ts b/apps/customer-portal/microapp/src/store/user.ts new file mode 100644 index 000000000..f06c9fef8 --- /dev/null +++ b/apps/customer-portal/microapp/src/store/user.ts @@ -0,0 +1,50 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { create } from "zustand"; + +export interface User { + email: string; + name: string; +} + +export interface UserStore { + user: User | null; + isLoading: boolean; + + setUser: (user: User) => void; + clearUser: () => void; + setLoading: (loading: boolean) => void; +} + +export const useUserStore = create((set) => ({ + user: null, + isLoading: false, + + setUser: (user: User) => + set({ + user, + isLoading: false, + }), + + clearUser: () => + set({ + user: null, + isLoading: false, + }), + + setLoading: (loading: boolean) => set({ isLoading: loading }), +})); diff --git a/apps/customer-portal/microapp/src/theme/index.ts b/apps/customer-portal/microapp/src/theme/index.ts new file mode 100644 index 000000000..142dc51ac --- /dev/null +++ b/apps/customer-portal/microapp/src/theme/index.ts @@ -0,0 +1,29 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { createTheme } from "@mui/material"; +import { palette } from "@theme/palette"; +import { typography } from "@theme/typography"; +import ComponentOverrides from "@theme/overrides"; + +const theme = createTheme({ + palette, + typography, +}); + +theme.components = ComponentOverrides(theme); + +export default theme; diff --git a/apps/customer-portal/microapp/src/theme/overrides/buttons.ts b/apps/customer-portal/microapp/src/theme/overrides/buttons.ts new file mode 100644 index 000000000..4ed738580 --- /dev/null +++ b/apps/customer-portal/microapp/src/theme/overrides/buttons.ts @@ -0,0 +1,58 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import type { Theme, Components } from "@mui/material"; + +export default function Buttons(theme: Theme): Components { + return { + MuiButtonBase: { + styleOverrides: { + root: { + padding: 10, + borderRadius: 8, + fontSize: theme.typography.button.fontSize, + display: "flex", + gap: 7, + variants: [ + { + props: { variant: "outlined" }, + style: { + background: theme.palette.background.default, + color: theme.palette.text.primary, + border: `1px solid ${theme.palette.semantic.border.subtle}`, + }, + }, + { + props: { variant: "contained" }, + style: { + color: theme.palette.primary.contrastText, + background: theme.palette.primary.main, + borderColor: theme.palette.primary.main, + borderWidth: 1, + }, + }, + ], + }, + }, + }, + }; +} + +declare module "@mui/material/ButtonBase" { + interface ButtonBaseOwnProps { + variant?: "outlined" | "contained"; + } +} diff --git a/apps/customer-portal/microapp/src/theme/overrides/chips.ts b/apps/customer-portal/microapp/src/theme/overrides/chips.ts new file mode 100644 index 000000000..5dfe5b0fa --- /dev/null +++ b/apps/customer-portal/microapp/src/theme/overrides/chips.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import type { Theme, Components, ChipOwnProps } from "@mui/material"; + +export default function Chips(theme: Theme): Components { + return { + MuiChip: { + styleOverrides: { + root: ({ ownerState }: { ownerState?: ChipOwnProps }) => { + const iconPosition = ownerState?.iconPosition ?? "start"; + + return { + display: "flex", + gap: 5, + + ...(iconPosition === "end" && { + flexDirection: "row-reverse", + "& .MuiChip-icon": { + gap: 0, + marginRight: 10, + marginLeft: 0, + }, + }), + }; + }, + }, + variants: [ + { + props: { color: "success" }, + style: { + color: theme.palette.semantic.chip.success.text, + backgroundColor: theme.palette.semantic.chip.success.background, + }, + }, + { + props: { color: "warning" }, + style: { + color: theme.palette.semantic.chip.warning.text, + backgroundColor: theme.palette.semantic.chip.warning.background, + }, + }, + ], + }, + }; +} + +declare module "@mui/material/Chip" { + interface ChipOwnProps { + iconPosition?: "start" | "end"; + } +} diff --git a/apps/customer-portal/microapp/src/theme/overrides/index.ts b/apps/customer-portal/microapp/src/theme/overrides/index.ts new file mode 100644 index 000000000..f34a58fe4 --- /dev/null +++ b/apps/customer-portal/microapp/src/theme/overrides/index.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import type { Theme } from "@mui/material"; +import Buttons from "@theme/overrides/buttons"; +import Inputs from "@theme/overrides/inputs"; +import Navigations from "@theme/overrides/navigations"; +import Chips from "./chips"; + +export default function ComponentOverrides(theme: Theme) { + return Object.assign(Buttons(theme), Inputs(theme), Navigations(theme), Chips(theme)); +} diff --git a/apps/customer-portal/microapp/src/theme/overrides/inputs.ts b/apps/customer-portal/microapp/src/theme/overrides/inputs.ts new file mode 100644 index 000000000..caae4c8fa --- /dev/null +++ b/apps/customer-portal/microapp/src/theme/overrides/inputs.ts @@ -0,0 +1,47 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import type { Theme, Components } from "@mui/material"; + +export default function Inputs(theme: Theme): Components { + return { + MuiInputBase: { + styleOverrides: { + root: { + padding: 2, + paddingLeft: 8, + paddingRight: 8, + borderRadius: 8, + fontSize: theme.typography.subtitle1.fontSize, + outline: `1px solid ${theme.palette.semantic.border.subtle}`, + transition: "outline-color 0.2s ease", + + "& .MuiInputBase-input": { + marginLeft: 2, + }, + + "&.Mui-focused": { + outline: `1.5px solid ${theme.palette.primary.main}`, + }, + + "& .MuiInputAdornment-root .MuiSvgIcon-root": { + fontSize: theme.typography.pxToRem(19), + }, + }, + }, + }, + }; +} diff --git a/apps/customer-portal/microapp/src/theme/overrides/navigations.tsx b/apps/customer-portal/microapp/src/theme/overrides/navigations.tsx new file mode 100644 index 000000000..76dac8970 --- /dev/null +++ b/apps/customer-portal/microapp/src/theme/overrides/navigations.tsx @@ -0,0 +1,44 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { type Theme, type Components, Typography } from "@mui/material"; + +export default function Navigations(theme: Theme): Components { + return { + MuiPaginationItem: { + defaultProps: { + slots: { + previous: () => Previous, + next: () => Next, + }, + }, + styleOverrides: { + root: { + "&.Mui-selected": { + color: theme.palette.common.white, + }, + }, + }, + }, + MuiPagination: { + defaultProps: { + shape: "rounded", + color: "primary", + siblingCount: 0, + }, + }, + }; +} diff --git a/apps/customer-portal/microapp/src/theme/palette.ts b/apps/customer-portal/microapp/src/theme/palette.ts new file mode 100644 index 000000000..6bd788467 --- /dev/null +++ b/apps/customer-portal/microapp/src/theme/palette.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +export const palette = { + primary: { + main: "#ff7300", + contrastText: "#ffffff", + }, + semantic: { + border: { + subtle: "#e0e0e0", + }, + portal: { + background: { main: "#f5f5f5", secondary: "#f9fafb" }, + accent: { + orange: "#ff5722", + green: "#4caf50", + blue: "#5b6ef5", + cyan: "#00bcd4", + purple: "#9c27b0", + amber: "#ffc107", + }, + }, + chip: { + success: { + text: "#4daf50", + background: "#e8f5e9", + }, + warning: { + text: "#ffc107", + background: "#fff8e1", + }, + }, + priority: { + low: { + background: "#f3f4f6", + text: "#374151", + }, + normal: { + background: "#dbeafe", + text: "#1d4ed8", + }, + high: { + background: "#fee2e2", + text: "#b91c1c", + }, + }, + status: { + wip: "#f97316", + waiting: "#3b82f6", + awaiting: "#a855f7", + }, + avatar: { + foreground: "#ca3500", + background: "#ffedd4", + }, + }, + divider: "#eeeeee", +} as const; + +declare module "@mui/material/styles" { + interface Palette { + semantic: typeof palette.semantic; + } +} diff --git a/apps/customer-portal/microapp/src/theme/typography.ts b/apps/customer-portal/microapp/src/theme/typography.ts new file mode 100644 index 000000000..af64496ef --- /dev/null +++ b/apps/customer-portal/microapp/src/theme/typography.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +export const pxToRem = (px: number) => `${px / 16}rem`; + +export const typography = { + fontFamily: '"Plus Jakarta Sans", "Roboto", "Arial", sans-serif', + fontSize: 14, + fontWeightLight: 300, + fontWeightRegular: 400, + fontWeightMedium: 550, + fontWeightBold: 600, + h1: { + fontSize: pxToRem(40), + }, + h2: { + fontSize: pxToRem(32), + }, + h3: { + fontSize: pxToRem(28), + }, + h4: { + fontSize: pxToRem(23), + }, + h5: { + fontSize: pxToRem(20), + }, + h6: { + fontSize: pxToRem(17), + }, + subtitle1: { + fontSize: pxToRem(15), + }, + subtitle2: { + fontSize: pxToRem(14), + }, + body1: { + fontSize: pxToRem(16), + }, + body2: { + fontSize: pxToRem(15.5), + }, + button: { + fontSize: pxToRem(16), + }, + caption: { + fontSize: pxToRem(12), + }, + overline: { + fontSize: pxToRem(11), + }, +}; diff --git a/apps/customer-portal/microapp/src/utils/constants.ts b/apps/customer-portal/microapp/src/utils/constants.ts new file mode 100644 index 000000000..90b83c9c5 --- /dev/null +++ b/apps/customer-portal/microapp/src/utils/constants.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +export const ErrorMessages = { + NATIVE_BRIDGE_NOT_AVAILABLE: "Native bridge is not available", +}; + +export const LocalStorageKeys = { + accessToken: "accessToken", + idToken: "idToken", +}; diff --git a/apps/customer-portal/microapp/src/utils/logger.ts b/apps/customer-portal/microapp/src/utils/logger.ts new file mode 100644 index 000000000..693bbf6bf --- /dev/null +++ b/apps/customer-portal/microapp/src/utils/logger.ts @@ -0,0 +1,37 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { sendNativeLog } from "@components/microapp-bridge"; + +/** + * Logger class for logging messages to the React Native DevTools + */ +export class Logger { + // Info/Basic Logs + static info(message: string, data?: unknown) { + sendNativeLog(message, data, "info"); + } + + // Error Logs + static error(message: string, data?: unknown) { + sendNativeLog(message, data, "error"); + } + + // Warning Logs + static warn(message: string, data?: unknown) { + sendNativeLog(message, data, "warn"); + } +} diff --git a/apps/customer-portal/microapp/src/utils/others.ts b/apps/customer-portal/microapp/src/utils/others.ts new file mode 100644 index 000000000..e4accf677 --- /dev/null +++ b/apps/customer-portal/microapp/src/utils/others.ts @@ -0,0 +1,19 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +export const stringAvatar = (name: string) => { + return `${name.split(" ")[0][0]}${name.split(" ")[1][0]}`; +}; diff --git a/apps/customer-portal/microapp/src/vite-env.d.ts b/apps/customer-portal/microapp/src/vite-env.d.ts new file mode 100644 index 000000000..03eb28229 --- /dev/null +++ b/apps/customer-portal/microapp/src/vite-env.d.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +/// +/// + +type ViteTypeOptions = object; + +interface ImportMetaEnv { + readonly VITE_BACKEND_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/apps/customer-portal/microapp/tsconfig.app.json b/apps/customer-portal/microapp/tsconfig.app.json new file mode 100644 index 000000000..b889ccf70 --- /dev/null +++ b/apps/customer-portal/microapp/tsconfig.app.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2023", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noImplicitAny": false + }, + "include": ["src"], + "extends": "./tsconfig.paths.json" +} diff --git a/apps/customer-portal/microapp/tsconfig.json b/apps/customer-portal/microapp/tsconfig.json new file mode 100644 index 000000000..d32ff6820 --- /dev/null +++ b/apps/customer-portal/microapp/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] +} diff --git a/apps/customer-portal/microapp/tsconfig.node.json b/apps/customer-portal/microapp/tsconfig.node.json new file mode 100644 index 000000000..284dce6f4 --- /dev/null +++ b/apps/customer-portal/microapp/tsconfig.node.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": false, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noImplicitAny": false + }, + "include": ["vite.config.ts"] +} diff --git a/apps/customer-portal/microapp/tsconfig.paths.json b/apps/customer-portal/microapp/tsconfig.paths.json new file mode 100644 index 000000000..1e028feb7 --- /dev/null +++ b/apps/customer-portal/microapp/tsconfig.paths.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@root/*": ["./*"], + "@src/*": ["./src/*"], + "@components/*": ["./src/components/*"], + "@config/*": ["./src/config/*"], + "@features/*": ["./src/features/*"], + "@icons/*": ["./src/icons/*"], + "@pages/*": ["./src/pages/*"], + "@theme/*": ["./src/theme/*"], + "@utils/*": ["./src/utils/*"], + } + } +} diff --git a/apps/customer-portal/microapp/vite.config.ts b/apps/customer-portal/microapp/vite.config.ts new file mode 100644 index 000000000..fe33f6791 --- /dev/null +++ b/apps/customer-portal/microapp/vite.config.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2025 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; +import tsconfigPaths from "vite-tsconfig-paths"; +import svgr from "vite-plugin-svgr"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), svgr(), tsconfigPaths()], + server: { + port: 3000, + }, + resolve: { + alias: { + "@root": path.resolve(__dirname), + "@src": path.resolve(__dirname, "src"), + "@components": path.resolve(__dirname, "src/components"), + "@config": path.resolve(__dirname, "src/config"), + "@features": path.resolve(__dirname, "src/features"), + "@icons": path.resolve(__dirname, "src/icons"), + "@pages": path.resolve(__dirname, "src/pages"), + "@theme": path.resolve(__dirname, "src/theme"), + "@utils": path.resolve(__dirname, "src/utils"), + }, + }, +});