feat: implement Telebirr payment API and types, enhance auth storage with token management

This commit is contained in:
Estifo77
2026-07-23 14:32:30 +03:00
parent 7191bc6791
commit 7e2186e758
8 changed files with 142 additions and 34 deletions

View File

@@ -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>

View File

@@ -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; }

View 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;

View File

@@ -0,0 +1,10 @@
export interface TelebirrPayload {
orderid: string;
}
export interface TelebirrCreatePaymentResponse {
status: string;
paymentUrl: string;
}
export interface TelebirrResponse {
status: string;
}

View File

@@ -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";
},
});

View File

@@ -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";

View File

@@ -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;

View File

@@ -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();