From 7e2186e758e0081c744cde63db75e7cc5ab55f28 Mon Sep 17 00:00:00 2001 From: Estifo77 <139631617+Estifo77@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:32:30 +0300 Subject: [PATCH] feat: implement Telebirr payment API and types, enhance auth storage with token management --- .../pages/EndorsementReviewPage.tsx | 8 ++-- apps/backoffice/src/styles.css | 26 ++++++++++ .../app/features/payment/api/payment-api.ts | 33 +++++++++++++ .../src/app/features/payment/types/payment.ts | 10 ++++ apps/portal/src/app/store/index.ts | 29 ++++++++---- libs/auth/src/index.ts | 47 ++++++++++++++----- libs/auth/src/lib/store/auth.slice.ts | 6 ++- libs/auth/src/lib/utils/refresh-token.ts | 17 +++---- 8 files changed, 142 insertions(+), 34 deletions(-) create mode 100644 apps/portal/src/app/features/payment/api/payment-api.ts create mode 100644 apps/portal/src/app/features/payment/types/payment.ts diff --git a/apps/backoffice/src/app/features/endorsement/pages/EndorsementReviewPage.tsx b/apps/backoffice/src/app/features/endorsement/pages/EndorsementReviewPage.tsx index b99106295..39ad590b7 100644 --- a/apps/backoffice/src/app/features/endorsement/pages/EndorsementReviewPage.tsx +++ b/apps/backoffice/src/app/features/endorsement/pages/EndorsementReviewPage.tsx @@ -233,7 +233,7 @@ function ActionModal({ Officer Action} + title={Officer Action} size="md" radius="lg" > @@ -373,7 +373,7 @@ export function EndorsementReviewPage() { Review all tabs, then take an action: - @@ -401,7 +401,7 @@ export function EndorsementReviewPage() { }>Foreign CoC }>Documents }>Payment - }>Endorsement + }>Endorsement {/* ── Applicant ─────────────────────────────────────────── */} @@ -522,7 +522,7 @@ export function EndorsementReviewPage() { - +
Issued Endorsement STCW Reg I/10 — Flag State Endorsement diff --git a/apps/backoffice/src/styles.css b/apps/backoffice/src/styles.css index ad50c6afc..53f8105c8 100644 --- a/apps/backoffice/src/styles.css +++ b/apps/backoffice/src/styles.css @@ -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; } diff --git a/apps/portal/src/app/features/payment/api/payment-api.ts b/apps/portal/src/app/features/payment/api/payment-api.ts new file mode 100644 index 000000000..9fa9eddf1 --- /dev/null +++ b/apps/portal/src/app/features/payment/api/payment-api.ts @@ -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({ + query: (orderId) => `/payments/${orderId}/status`, + providesTags: ["Api"], + }), + getPaymentDetail: builder.query({ + 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; diff --git a/apps/portal/src/app/features/payment/types/payment.ts b/apps/portal/src/app/features/payment/types/payment.ts new file mode 100644 index 000000000..0cf92ca8e --- /dev/null +++ b/apps/portal/src/app/features/payment/types/payment.ts @@ -0,0 +1,10 @@ +export interface TelebirrPayload { + orderid: string; +} +export interface TelebirrCreatePaymentResponse { + status: string; + paymentUrl: string; +} +export interface TelebirrResponse { + status: string; +} diff --git a/apps/portal/src/app/store/index.ts b/apps/portal/src/app/store/index.ts index a48e46406..88a04e5aa 100644 --- a/apps/portal/src/app/store/index.ts +++ b/apps/portal/src/app/store/index.ts @@ -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(); const profile = authStorage.getProfile(); 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"; }, }); diff --git a/libs/auth/src/index.ts b/libs/auth/src/index.ts index f1919dd50..31b81284d 100644 --- a/libs/auth/src/index.ts +++ b/libs/auth/src/index.ts @@ -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"; diff --git a/libs/auth/src/lib/store/auth.slice.ts b/libs/auth/src/lib/store/auth.slice.ts index 5ae8afb6c..1fb70a533 100644 --- a/libs/auth/src/lib/store/auth.slice.ts +++ b/libs/auth/src/lib/store/auth.slice.ts @@ -42,7 +42,10 @@ const authSlice = createSlice({ state.isAuthenticated = false; state.currentProfile = null; authStorage.clear(); - authStorage.clear(); + }, + setToken(state, action: PayloadAction) { + 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; diff --git a/libs/auth/src/lib/utils/refresh-token.ts b/libs/auth/src/lib/utils/refresh-token.ts index a809ad268..91daa7f1a 100644 --- a/libs/auth/src/lib/utils/refresh-token.ts +++ b/libs/auth/src/lib/utils/refresh-token.ts @@ -1,8 +1,9 @@ -import { authStorage } from './auth-storage'; +import { authStorage } from "./auth-storage"; const BASE_API_URL = - (import.meta as { env?: Record }).env?.['VITE_BASE_API_URL'] ?? - 'http://localhost:3001/api'; + (import.meta as { env?: Record }).env?.[ + "VITE_BASE_API_URL" + ] ?? "http://localhost:3001/api"; interface RefreshResponse { token: string; @@ -11,17 +12,17 @@ interface RefreshResponse { export async function refreshAccessToken(): Promise { 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();