mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-27 01:22:49 +00:00
feat: implement Telebirr payment API and types, enhance auth storage with token management
This commit is contained in:
@@ -233,7 +233,7 @@ function ActionModal({
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={<Group gap="xs"><IconStamp size={17} /><Text fw={700}>Officer Action</Text></Group>}
|
||||
title={<Group gap="xs"><IconRubberStamp size={17} /><Text fw={700}>Officer Action</Text></Group>}
|
||||
size="md"
|
||||
radius="lg"
|
||||
>
|
||||
@@ -373,7 +373,7 @@ export function EndorsementReviewPage() {
|
||||
<Paper withBorder radius="lg" p="md" bg="gray.0">
|
||||
<Group gap="sm" justify="flex-end">
|
||||
<Text fz="sm" c="dimmed" style={{ flex: 1 }}>Review all tabs, then take an action:</Text>
|
||||
<Button size="sm" leftSection={<IconStamp size={15} />} onClick={() => setActionModalOpen(true)}>
|
||||
<Button size="sm" leftSection={<IconRubberStamp size={15} />} onClick={() => setActionModalOpen(true)}>
|
||||
Take Action
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -401,7 +401,7 @@ export function EndorsementReviewPage() {
|
||||
<Tabs.Tab value="foreign-coc" leftSection={<IconShieldCheck size={16} />}>Foreign CoC</Tabs.Tab>
|
||||
<Tabs.Tab value="documents" leftSection={<IconFileDescription size={16} />}>Documents</Tabs.Tab>
|
||||
<Tabs.Tab value="payment" leftSection={<IconCreditCard size={16} />}>Payment</Tabs.Tab>
|
||||
<Tabs.Tab value="endorsement" leftSection={<IconStamp size={16} />}>Endorsement</Tabs.Tab>
|
||||
<Tabs.Tab value="endorsement" leftSection={<IconRubberStamp size={16} />}>Endorsement</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
{/* ── Applicant ─────────────────────────────────────────── */}
|
||||
@@ -522,7 +522,7 @@ export function EndorsementReviewPage() {
|
||||
</Alert>
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<Group gap="sm" mb="lg">
|
||||
<ThemeIcon size="xl" variant="light" color="teal" radius="lg"><IconStamp size={22} stroke={1.5} /></ThemeIcon>
|
||||
<ThemeIcon size="xl" variant="light" color="teal" radius="lg"><IconRubberStamp size={22} stroke={1.5} /></ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700} fz="lg">Issued Endorsement</Text>
|
||||
<Text fz="xs" c="dimmed">STCW Reg I/10 — Flag State Endorsement</Text>
|
||||
|
||||
@@ -4,3 +4,29 @@
|
||||
@tailwind utilities;
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--ema-scrollbar-light: #c1c1c1;
|
||||
--ema-scrollbar-dark: #374151;
|
||||
--ema-scrollbar-track-light: #f5f8fc;
|
||||
--ema-scrollbar-track-dark: #0e1521;
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme='light'] body {
|
||||
--ema-scrollbar-thumb: var(--ema-scrollbar-light);
|
||||
--ema-scrollbar-track: var(--ema-scrollbar-track-light);
|
||||
}
|
||||
[data-mantine-color-scheme='dark'] body {
|
||||
--ema-scrollbar-thumb: var(--ema-scrollbar-dark);
|
||||
--ema-scrollbar-track: var(--ema-scrollbar-track-dark);
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--ema-scrollbar-thumb) var(--ema-scrollbar-track);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: var(--ema-scrollbar-track); }
|
||||
::-webkit-scrollbar-thumb { background: var(--ema-scrollbar-thumb); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { opacity: 0.8; }
|
||||
|
||||
33
apps/portal/src/app/features/payment/api/payment-api.ts
Normal file
33
apps/portal/src/app/features/payment/api/payment-api.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { baseApi } from "@ema-platform/api";
|
||||
import {
|
||||
TelebirrCreatePaymentResponse,
|
||||
TelebirrPayload,
|
||||
TelebirrResponse,
|
||||
} from "../types/payment";
|
||||
const paymentApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
getPaymentStatusTelebirr: builder.query<TelebirrResponse, void>({
|
||||
query: (orderId) => `/payments/${orderId}/status`,
|
||||
providesTags: ["Api"],
|
||||
}),
|
||||
getPaymentDetail: builder.query<TelebirrResponse, void>({
|
||||
query: (paymentId) => `/payments/${paymentId}`,
|
||||
providesTags: ["Api"],
|
||||
}),
|
||||
createPaymentTelebirr: builder.mutation<
|
||||
TelebirrCreatePaymentResponse,
|
||||
TelebirrPayload
|
||||
>({
|
||||
query: (body) => ({
|
||||
url: "/payments/telebirr/create",
|
||||
method: "POST",
|
||||
body,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
export const {
|
||||
useCreatePaymentTelebirrMutation,
|
||||
useGetPaymentStatusTelebirrQuery,
|
||||
useGetPaymentDetailQuery,
|
||||
} = paymentApi;
|
||||
10
apps/portal/src/app/features/payment/types/payment.ts
Normal file
10
apps/portal/src/app/features/payment/types/payment.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface TelebirrPayload {
|
||||
orderid: string;
|
||||
}
|
||||
export interface TelebirrCreatePaymentResponse {
|
||||
status: string;
|
||||
paymentUrl: string;
|
||||
}
|
||||
export interface TelebirrResponse {
|
||||
status: string;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { baseApi, configureTokenRefresh } from '@ema-platform/api';
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { baseApi, configureTokenRefresh } from "@ema-platform/api";
|
||||
import { setToken } from "@ema-platform/auth";
|
||||
import {
|
||||
authReducer,
|
||||
signupReducer,
|
||||
@@ -7,17 +8,22 @@ import {
|
||||
authStorage,
|
||||
refreshAccessToken,
|
||||
logout,
|
||||
} from '@ema-platform/auth';
|
||||
import type { AuthUser, CurrentProfile } from '@ema-platform/auth';
|
||||
} from "@ema-platform/auth";
|
||||
import type { AuthUser, CurrentProfile } from "@ema-platform/auth";
|
||||
|
||||
configureAuthStorage('ema-portal');
|
||||
configureAuthStorage("ema-portal");
|
||||
|
||||
const preloadedAuth = (() => {
|
||||
const token = authStorage.getToken();
|
||||
const user = authStorage.getUser<AuthUser>();
|
||||
const profile = authStorage.getProfile<CurrentProfile>();
|
||||
if (token && user) {
|
||||
return { token, user, isAuthenticated: true, currentProfile: profile ?? null };
|
||||
return {
|
||||
token,
|
||||
user,
|
||||
isAuthenticated: true,
|
||||
currentProfile: profile ?? null,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
})();
|
||||
@@ -34,10 +40,17 @@ export const store = configureStore({
|
||||
});
|
||||
|
||||
configureTokenRefresh({
|
||||
onTokenExpired: refreshAccessToken,
|
||||
onTokenExpired: async () => {
|
||||
const token = await refreshAccessToken();
|
||||
|
||||
store.dispatch(setToken(token));
|
||||
|
||||
return token;
|
||||
},
|
||||
|
||||
onAuthFailure: () => {
|
||||
store.dispatch(logout());
|
||||
window.location.href = '/login';
|
||||
window.location.href = "/login";
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
export { AuthConfigProvider, useAuthConfig } from './lib/AuthConfig';
|
||||
export type { AuthConfigValue } from './lib/AuthConfig';
|
||||
export { AuthShell, BrandMark } from './lib/components/AuthShell';
|
||||
export { ProtectedRoute } from './lib/components/ProtectedRoute';
|
||||
export { LoginPage } from './lib/pages/LoginPage';
|
||||
export { SignupPage } from './lib/pages/SignupPage';
|
||||
export { ForgotPasswordPage } from './lib/pages/ForgotPasswordPage';
|
||||
export { OTPVerificationPage } from './lib/pages/OTPVerificationPage';
|
||||
export { authReducer, loginSuccess, setUser, setCurrentProfile, clearCurrentProfile, logout, hydrateAuth } from './lib/store/auth.slice';
|
||||
export { signupReducer, setSignupData, setSignupStep, resetSignup } from './lib/store/signup.slice';
|
||||
export { configureAuthStorage, authStorage } from './lib/utils/auth-storage';
|
||||
export { refreshAccessToken } from './lib/utils/refresh-token';
|
||||
export type { AuthUser, AuthState, LoginPayload, CurrentProfile, CurrentProfileAddress, CurrentProfileProfession } from './lib/types/auth.types';
|
||||
export { AuthConfigProvider, useAuthConfig } from "./lib/AuthConfig";
|
||||
export type { AuthConfigValue } from "./lib/AuthConfig";
|
||||
export { AuthShell, BrandMark } from "./lib/components/AuthShell";
|
||||
export { ProtectedRoute } from "./lib/components/ProtectedRoute";
|
||||
export { LoginPage } from "./lib/pages/LoginPage";
|
||||
export { SignupPage } from "./lib/pages/SignupPage";
|
||||
export { ForgotPasswordPage } from "./lib/pages/ForgotPasswordPage";
|
||||
export { OTPVerificationPage } from "./lib/pages/OTPVerificationPage";
|
||||
export {
|
||||
authReducer,
|
||||
loginSuccess,
|
||||
setUser,
|
||||
setCurrentProfile,
|
||||
clearCurrentProfile,
|
||||
logout,
|
||||
hydrateAuth,
|
||||
setToken,
|
||||
} from "./lib/store/auth.slice";
|
||||
export {
|
||||
signupReducer,
|
||||
setSignupData,
|
||||
setSignupStep,
|
||||
resetSignup,
|
||||
} from "./lib/store/signup.slice";
|
||||
export { configureAuthStorage, authStorage } from "./lib/utils/auth-storage";
|
||||
export { refreshAccessToken } from "./lib/utils/refresh-token";
|
||||
export type {
|
||||
AuthUser,
|
||||
AuthState,
|
||||
LoginPayload,
|
||||
CurrentProfile,
|
||||
CurrentProfileAddress,
|
||||
CurrentProfileProfession,
|
||||
} from "./lib/types/auth.types";
|
||||
|
||||
@@ -42,7 +42,10 @@ const authSlice = createSlice({
|
||||
state.isAuthenticated = false;
|
||||
state.currentProfile = null;
|
||||
authStorage.clear();
|
||||
authStorage.clear();
|
||||
},
|
||||
setToken(state, action: PayloadAction<string>) {
|
||||
state.token = action.payload;
|
||||
authStorage.setToken(action.payload);
|
||||
},
|
||||
hydrateAuth(state) {
|
||||
const token = authStorage.getToken();
|
||||
@@ -67,5 +70,6 @@ export const {
|
||||
clearCurrentProfile,
|
||||
logout,
|
||||
hydrateAuth,
|
||||
setToken,
|
||||
} = authSlice.actions;
|
||||
export const authReducer = authSlice.reducer;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { authStorage } from './auth-storage';
|
||||
import { authStorage } from "./auth-storage";
|
||||
|
||||
const BASE_API_URL =
|
||||
(import.meta as { env?: Record<string, string> }).env?.['VITE_BASE_API_URL'] ??
|
||||
'http://localhost:3001/api';
|
||||
(import.meta as { env?: Record<string, string> }).env?.[
|
||||
"VITE_BASE_API_URL"
|
||||
] ?? "http://localhost:3001/api";
|
||||
|
||||
interface RefreshResponse {
|
||||
token: string;
|
||||
@@ -11,17 +12,17 @@ interface RefreshResponse {
|
||||
|
||||
export async function refreshAccessToken(): Promise<string> {
|
||||
const refreshToken = authStorage.getRefreshToken();
|
||||
if (!refreshToken) throw new Error('No refresh token available');
|
||||
if (!refreshToken) throw new Error("No refresh token available");
|
||||
|
||||
const response = await fetch(`${BASE_API_URL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
const response = await fetch(`${BASE_API_URL}/auth/refresh-token`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
authStorage.clear();
|
||||
throw new Error('Token refresh failed');
|
||||
throw new Error("Token refresh failed");
|
||||
}
|
||||
|
||||
const data: RefreshResponse = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user