-
-
-
Erreur de connexion
-
{error}
-
-
- {retryCount < maxRetries && (
-
-
- Réessayer ({retryCount + 1}/{maxRetries + 1})
-
- )}
-
-
-
- Forcer le rechargement
-
-
-
+ {!isIframeReady && (
+
+
+
Chargement de l'authentification...
+
+ )}
+ {authSuccess ? (
+
+
+
+ Authentification réussie !
- )}
-
- {isLoading && !authSuccess && (
-
-
-
{loadingStep}
- {loadingStep &&
Protocole 4NK en cours...
}
-
- {/* Barre de progression visuelle */}
-
-
- )}
-
- {authSuccess && (
-
-
-
Authentification réussie !
-
Tokens stockés • Redirection en cours...
-
- )}
-
- {showIframe && !authSuccess && !error && (
-
-
-
Interface d'authentification 4NK
-
En attente de LISTENING → REQUEST_LINK → LINK_ACCEPTED
-
-
-
- )}
-
- {/* Informations de debug */}
- {(error || isLoading) && (
-
-
- URL: {iframeUrl}
-
-
- Tentative: {retryCount + 1}/{maxRetries + 1}
-
-
- Iframe chargée: {iframeLoaded ? "Oui" : "Non"}
-
-
- Protocole: LISTENING → REQUEST_LINK → LINK_ACCEPTED
-
+ ) : (
+
+
)}
diff --git a/components/4nk/Iframe.tsx b/components/4nk/Iframe.tsx
index 18ff21f..7d5203e 100644
--- a/components/4nk/Iframe.tsx
+++ b/components/4nk/Iframe.tsx
@@ -1,7 +1,7 @@
"use client"
import { useRef, useEffect, memo } from "react"
-import { IframeReference } from "@/lib/4nk/IframeReference"
+import IframeReference from "@/lib/4nk/IframeReference"
interface IframeProps {
iframeUrl: string
@@ -19,19 +19,19 @@ export const Iframe = memo(function Iframe({ iframeUrl, showIframe = false }: If
return () => {
IframeReference.setIframe(null)
}
- }, [])
+ }, [iframeRef.current])
return (
)
diff --git a/lib/4nk/EventBus.ts b/lib/4nk/EventBus.ts
index 8b08e5b..20e8c28 100644
--- a/lib/4nk/EventBus.ts
+++ b/lib/4nk/EventBus.ts
@@ -1,47 +1,33 @@
-/**
- * EventBus - Bus d'événements intra-onglet pour la communication interne
- * Pattern Singleton avec pub/sub
- */
-export class EventBus {
- private static instance: EventBus | null = null
- private listeners: Map
void>> = new Map()
+export default class EventBus {
+ private static instance: EventBus;
+ private listeners: Record void>> = {};
- private constructor() {}
+ private constructor() { }
- static getInstance(): EventBus {
+ public static getInstance(): EventBus {
if (!EventBus.instance) {
- EventBus.instance = new EventBus()
+ EventBus.instance = new EventBus();
}
- return EventBus.instance
+ return EventBus.instance;
}
- on(event: string, callback: (...args: any[]) => void): () => void {
- if (!this.listeners.has(event)) {
- this.listeners.set(event, [])
+ public on(event: string, callback: (...args: any[]) => void): () => void {
+ if (!this.listeners[event]) {
+ this.listeners[event] = [];
}
-
- const eventListeners = this.listeners.get(event)!
- eventListeners.push(callback)
-
- // Retourne une fonction d'unsubscribe
+ this.listeners[event].push(callback);
return () => {
- const index = eventListeners.indexOf(callback)
- if (index > -1) {
- eventListeners.splice(index, 1)
+ if (this.listeners[event]) {
+ this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
}
- }
+ };
}
- emit(event: string, ...args: any[]): void {
- const eventListeners = this.listeners.get(event)
- if (eventListeners) {
- eventListeners.forEach((callback) => {
- try {
- callback(...args)
- } catch (error) {
- console.error(`Error in event listener for ${event}:`, error)
- }
- })
+ public emit(event: string, ...args: any[]): void {
+ if (this.listeners[event]) {
+ this.listeners[event].forEach(callback => {
+ callback(...args);
+ });
}
}
}
diff --git a/lib/4nk/IframeReference.ts b/lib/4nk/IframeReference.ts
index f27cfd3..90dde57 100644
--- a/lib/4nk/IframeReference.ts
+++ b/lib/4nk/IframeReference.ts
@@ -1,15 +1,13 @@
-/**
- * IframeReference - Référence globale pour l'iframe 4NK
- * Permet aux autres services d'accéder à l'iframe pour postMessage
- */
-export class IframeReference {
- private static iframe: HTMLIFrameElement | null = null
+export default class IframeReference {
+ private static iframe: HTMLIFrameElement | null = null;
- static setIframe(iframe: HTMLIFrameElement | null): void {
- IframeReference.iframe = iframe
+ private constructor() { }
+
+ public static setIframe(iframe: HTMLIFrameElement | null): void {
+ this.iframe = iframe;
}
- static getIframe(): HTMLIFrameElement | null {
- return IframeReference.iframe
+ public static getIframe(): HTMLIFrameElement | null {
+ return this.iframe;
}
}
diff --git a/lib/4nk/MessageBus.ts b/lib/4nk/MessageBus.ts
index 2ffe5da..b8af7b6 100644
--- a/lib/4nk/MessageBus.ts
+++ b/lib/4nk/MessageBus.ts
@@ -1,559 +1,746 @@
-import { v4 as uuidv4 } from "uuid"
-import { IframeReference } from "./IframeReference"
-import { EventBus } from "./EventBus"
-import { UserStore } from "./UserStore"
-import { MockService } from "./MockService"
+import IframeReference from './IframeReference';
+import EventBus from './EventBus';
+import UserStore from './UserStore';
+import { v4 as uuidv4 } from 'uuid';
-/**
- * MessageBus - Passerelle de communication avec l'iframe 4NK
- * Supporte le mode mock avec l'identifiant d'entreprise "1234"
- */
-export class MessageBus {
- private static instance: MessageBus | null = null
- private origin: string
- private eventBus: EventBus
- private userStore: UserStore
- private mockService: MockService
- private errors: Map = new Map()
- private isMockMode = false
+export default class MessageBus {
+ private static instance: MessageBus;
+ private readonly origin: string;
+ private messageListener: ((event: MessageEvent) => void) | null = null;
+ private errors: { [key: string]: string } = {};
+ private readyPromise: Promise | null = null;
+ private isReadyFlag = false;
private constructor(origin: string) {
- this.origin = origin
- this.eventBus = EventBus.getInstance()
- this.userStore = UserStore.getInstance()
- this.mockService = MockService.getInstance()
-
- console.log("🔧 MessageBus initialized with origin:", origin)
+ this.origin = origin;
}
- static getInstance(origin: string): MessageBus {
+ public static getInstance(origin: string): MessageBus {
if (!MessageBus.instance) {
- MessageBus.instance = new MessageBus(origin)
+ MessageBus.instance = new MessageBus(origin);
}
- return MessageBus.instance
+ return MessageBus.instance;
}
- // Activer le mode mock
- enableMockMode(): void {
- this.isMockMode = true
- console.log("🎭 Mock mode enabled")
- }
-
- // Désactiver le mode mock
- disableMockMode(): void {
- this.isMockMode = false
- this.mockService.disconnect()
- console.log("🎭 Mock mode disabled")
- }
-
- isInMockMode(): boolean {
- return this.isMockMode
- }
-
- private getOriginFromUrl(url: string): string {
- try {
- const urlObj = new URL(url)
- return `${urlObj.protocol}//${urlObj.host}`
- } catch (error) {
- console.error("Invalid URL:", url)
- return url
- }
- }
-
- private sendMessage(type: string, data: any = {}, timeoutMs = 30000): Promise {
- // Si en mode mock, utiliser le service mock
- if (this.isMockMode) {
- return this.handleMockMessage(type, data)
+ public isReady(): Promise {
+ if (this.isReadyFlag) {
+ return Promise.resolve();
}
- return new Promise((resolve, reject) => {
- const messageId = uuidv4()
- const iframe = IframeReference.getIframe()
+ if (this.readyPromise) {
+ return this.readyPromise;
+ }
- if (!iframe || !iframe.contentWindow) {
- reject(new Error("Iframe not available"))
- return
+ this.readyPromise = new Promise((resolve) => {
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('IS_READY', (responseId: string) => {
+ if (responseId !== correlationId) return;
+ unsubscribe();
+ this.destroyMessageListener();
+ this.isReadyFlag = true;
+ resolve();
+ });
+ });
+
+ return this.readyPromise;
+ }
+
+ public requestLink(): Promise {
+ return new Promise((resolve: () => void, reject: (error: string) => void) => {
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('LINK_ACCEPTED', (responseId: string, accessToken: string, refreshToken: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ UserStore.getInstance().connect(accessToken, refreshToken);
+ resolve();
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_LINK_ACCEPTED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'REQUEST_LINK'
+ });
+ });
+ }
+
+ public getUserPairingId(): Promise {
+ return new Promise((resolve: (userPairingId: string) => void, reject: (error: string) => void) => {
+ this.checkToken().then(() => {
+ const userStore = UserStore.getInstance();
+ const accessToken = userStore.getAccessToken()!;
+
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('PAIRING_ID', (responseId: string, userPairingId: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ resolve(userPairingId);
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_PAIRING_ID', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'GET_PAIRING_ID',
+ accessToken,
+ });
+ }).catch(console.error);
+ });
+ }
+
+ public validateToken(): Promise {
+ return new Promise((resolve: (isValid: boolean) => void, reject: (error: string) => void) => {
+ const userStore = UserStore.getInstance();
+ if (!userStore.isConnected()) {
+ reject('User is not connected');
+ return;
}
+ const accessToken = userStore.getAccessToken()!;
+ const refreshToken = userStore.getRefreshToken()!;
- const targetOrigin = this.getOriginFromUrl(this.origin)
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
- // Construire le message selon le protocole 4NK
- const message: any = {
- type,
- messageId,
- timestamp: Date.now(),
- ...data,
+ const unsubscribe = EventBus.getInstance().on('TOKEN_VALIDATED', (responseId: string, isValid: boolean) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ resolve(isValid);
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_TOKEN_VALIDATED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'VALIDATE_TOKEN',
+ accessToken,
+ refreshToken
+ });
+ });
+ }
+
+ public renewToken(): Promise {
+ return new Promise((resolve: () => void, reject: (error: string) => void) => {
+ const userStore = UserStore.getInstance();
+ if (!userStore.isConnected()) {
+ reject('User is not connected');
+ return;
}
+ const refreshToken = userStore.getRefreshToken()!;
- // Ajouter l'accessToken pour tous les messages sauf REQUEST_LINK
- if (type !== "REQUEST_LINK") {
- const accessToken = this.userStore.getAccessToken()
- if (accessToken) {
- message.accessToken = accessToken
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('TOKEN_RENEWED', (responseId: string, accessToken: string, refreshToken: string) => {
+ if (responseId !== correlationId) {
+ return;
}
- }
+ unsubscribe();
+ this.destroyMessageListener();
+ UserStore.getInstance().connect(accessToken, refreshToken);
+ resolve();
+ });
- console.log(`📤 Sending message ${type} to origin:`, targetOrigin)
- console.log(`📤 Message data:`, {
- type,
- messageId,
- hasAccessToken: !!message.accessToken,
- })
-
- const cleanup = this.initMessageListener(messageId, resolve, reject, timeoutMs)
-
- // Essayer d'envoyer le message avec plusieurs stratégies
- const sendStrategies = [
- () => iframe.contentWindow!.postMessage(message, targetOrigin),
- () => iframe.contentWindow!.postMessage(message, "*"),
- ]
-
- let strategyIndex = 0
- const trySend = () => {
- if (strategyIndex >= sendStrategies.length) {
- cleanup()
- reject(new Error("Impossible d'envoyer le message après plusieurs tentatives"))
- return
+ const unsubscribeError = EventBus.getInstance().on('ERROR_TOKEN_RENEWED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
}
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
- try {
- sendStrategies[strategyIndex]()
- console.log(`✅ Message sent with strategy ${strategyIndex + 1}`)
- } catch (error) {
- console.error(`❌ Strategy ${strategyIndex + 1} failed:`, error)
- strategyIndex++
- setTimeout(trySend, 500)
+ this.sendMessage({
+ type: 'RENEW_TOKEN',
+ refreshToken
+ });
+ });
+ }
+
+ public getProcesses(): Promise {
+ return new Promise((resolve: (processes: any) => void, reject: (error: string) => void) => {
+ this.checkToken().then(() => {
+ const userStore = UserStore.getInstance();
+ const accessToken = userStore.getAccessToken()!;
+
+ const correlationId = uuidv4();
+ console.log(correlationId);
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('PROCESSES_RETRIEVED', (responseId: string, processes: any) => {
+ console.log(responseId);
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ resolve(processes);
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESSES_RETRIEVED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'GET_PROCESSES',
+ accessToken,
+ });
+ }).catch(console.error);
+ });
+ }
+
+ public getMyProcesses(): Promise {
+ return new Promise((resolve: (myProcesses: string[]) => void, reject: (error: string) => void) => {
+ this.checkToken().then(() => {
+ const userStore = UserStore.getInstance();
+ const accessToken = userStore.getAccessToken()!;
+
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('GET_MY_PROCESSES', (responseId: string, myProcesses: string[]) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ resolve(myProcesses);
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_GET_MY_PROCESSES', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'GET_MY_PROCESSES',
+ accessToken,
+ });
+ }).catch(console.error);
+ });
+ }
+
+ public getData(processId: string, stateId: string): Promise> {
+ return new Promise>((resolve: (data: Record) => void, reject: (error: string) => void) => {
+ this.checkToken().then(() => {
+ const userStore = UserStore.getInstance();
+ const accessToken = userStore.getAccessToken()!;
+
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('DATA_RETRIEVED', (responseId: string, data: Record) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ resolve(data);
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_DATA_RETRIEVED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'RETRIEVE_DATA',
+ processId,
+ stateId,
+ accessToken
+ });
+ }).catch(console.error);
+ });
+ }
+
+ public getPublicData(encodedData: number[]): Promise {
+ return new Promise((resolve: (data: any) => void, reject: (error: string) => void) => {
+ this.checkToken().then(() => {
+ const userStore = UserStore.getInstance();
+ const accessToken = userStore.getAccessToken()!;
+
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('PUBLIC_DATA_DECODED', (responseId: string, data: any) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ resolve(data);
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_PUBLIC_DATA_DECODED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'DECODE_PUBLIC_DATA',
+ encodedData,
+ accessToken
+ });
+ }).catch(console.error);
+ });
+ }
+
+ public createProfile(profileData: any, profilePrivateData: string[], roles: any): Promise {
+ return new Promise((resolve: (profileCreated: any) => void, reject: (error: string) => void) => {
+ this.checkToken().then(() => {
+ const userStore = UserStore.getInstance();
+ const accessToken = userStore.getAccessToken()!;
+
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('PROCESS_CREATED', (responseId: string, processCreated: any) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ // Return value must contain the data commited in the new process
+ const profileData = processCreated.processData;
+ if (!profileData) {
+ reject('Returned invalid profile data');
+ }
+ if (!processCreated.processId || typeof processCreated.processId !== 'string') {
+ console.error('Returned invalid process id');
+ reject('Returned invalid process id');
+ }
+ // TODO check that process is of type Process
+
+ const profileCreated = {
+ processId: processCreated.processId,
+ process: processCreated.process,
+ profileData
+ };
+
+ resolve(profileCreated);
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_CREATED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'CREATE_PROCESS',
+ processData: profileData,
+ privateFields: profilePrivateData,
+ roles,
+ accessToken
+ });
+ }).catch(console.error);
+ });
+ }
+
+ public createFolder(folderData: any, folderPrivateData: string[], roles: any): Promise {
+ return new Promise((resolve: (folderData: any) => void, reject: (error: string) => void) => {
+ this.checkToken().then(() => {
+ const userStore = UserStore.getInstance();
+ const accessToken = userStore.getAccessToken()!;
+
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('PROCESS_CREATED', (responseId: string, processCreated: any) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ // Return value must contain the data commited in the new process
+ const folderData = processCreated.processData;
+ if (!folderData) reject('Returned invalid process data');
+ if (!processCreated.processId || typeof processCreated.processId !== 'string') reject('Returned invalid process id');
+ // TODO check that process is of type Process
+
+ const folderCreated = {
+ processId: processCreated.processId,
+ process: processCreated.process,
+ folderData
+ };
+
+ resolve(folderCreated);
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_CREATED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'CREATE_PROCESS',
+ processData: folderData,
+ privateFields: folderPrivateData,
+ roles,
+ accessToken
+ });
+ }).catch(console.error);
+ });
+ }
+
+ public updateProcess(processId: string, lastStateId: string, newData: Record, privateFields: string[], roles: any | null): Promise {
+ return new Promise((resolve: (updatedProcess: any) => void, reject: (error: string) => void) => {
+ this.checkToken().then(() => {
+ const userStore = UserStore.getInstance();
+ const accessToken = userStore.getAccessToken()!;
+
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('PROCESS_UPDATED', (responseId: string, updatedProcess: any) => {
+ console.log('PROCESS_UPDATED', updatedProcess);
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ resolve(updatedProcess);
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_UPDATED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'UPDATE_PROCESS',
+ processId,
+ lastStateId,
+ newData,
+ privateFields,
+ roles,
+ accessToken
+ });
+ }).catch(console.error);
+ });
+ }
+
+ public notifyProcessUpdate(processId: string, stateId: string): Promise {
+ return new Promise((resolve: () => void, reject: (error: string) => void) => {
+ this.checkToken().then(() => {
+ const userStore = UserStore.getInstance();
+ const accessToken = userStore.getAccessToken()!;
+
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('UPDATE_NOTIFIED', (responseId: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ resolve();
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_UPDATE_NOTIFIED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'NOTIFY_UPDATE',
+ processId,
+ stateId,
+ accessToken
+ });
+ }).catch(console.error);
+ });
+ }
+
+ public validateState(processId: string, stateId: string): Promise {
+ return new Promise((resolve: (updatedProcess: any) => void, reject: (error: string) => void) => {
+ this.checkToken().then(() => {
+ const userStore = UserStore.getInstance();
+ const accessToken = userStore.getAccessToken()!;
+
+ const correlationId = uuidv4();
+ this.initMessageListener(correlationId);
+
+ const unsubscribe = EventBus.getInstance().on('STATE_VALIDATED', (responseId: string, updatedProcess: any) => {
+ console.log(updatedProcess);
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribe();
+ this.destroyMessageListener();
+ resolve(updatedProcess);
+ });
+
+ const unsubscribeError = EventBus.getInstance().on('ERROR_STATE_VALIDATED', (responseId: string, error: string) => {
+ if (responseId !== correlationId) {
+ return;
+ }
+ unsubscribeError();
+ this.destroyMessageListener();
+ reject(error);
+ });
+
+ this.sendMessage({
+ type: 'VALIDATE_STATE',
+ processId,
+ stateId,
+ accessToken
+ });
+ }).catch(console.error);
+ });
+ }
+
+ private checkToken(): Promise {
+ return new Promise((resolve: () => void, reject: (error: string) => void) => {
+ this.validateToken().then((isValid: boolean) => {
+ if (!isValid) {
+ this.renewToken().then(resolve).catch(reject);
+ } else {
+ resolve();
}
- }
-
- trySend()
- })
+ }).catch(reject);
+ });
}
- private async handleMockMessage(type: string, data: any): Promise {
- console.log(`🎭 Handling mock message: ${type}`)
+ private sendMessage(message: any): void {
+ const iframe = IframeReference.getIframe();
+ if (!iframe) {
+ console.error('[MessageBus] sendMessage: iframe not found');
+ return;
+ }
+ console.log('[MessageBus] sendMessage:', message);
+ iframe.contentWindow?.postMessage(message, this.origin);
+ }
- switch (type) {
- case "REQUEST_LINK":
- const tokens = await this.mockService.mockRequestLink()
- return {
- type: "LINK_ACCEPTED",
- accessToken: tokens.accessToken,
- refreshToken: tokens.refreshToken,
+ private initMessageListener(correlationId: string): void {
+ this.destroyMessageListener();
+
+ this.messageListener = this.handleMessage.bind(this, correlationId);
+ window.addEventListener('message', this.messageListener);
+ }
+
+ private destroyMessageListener(): void {
+ if (this.messageListener) {
+ window.removeEventListener('message', this.messageListener);
+ this.messageListener = null;
+ }
+ }
+
+ private handleMessage(correlationId: string, event: MessageEvent): void {
+ if (event.origin !== this.origin) {
+ return;
+ }
+
+ if (!event.data || typeof event.data !== 'object') {
+ return;
+ }
+
+ const message = event.data;
+ switch (message.type) {
+ case 'LISTENING':
+ EventBus.getInstance().emit('IS_READY', correlationId);
+ break;
+
+ case 'LINK_ACCEPTED':
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_LINK_ACCEPTED', correlationId, error);
+ return;
}
+ EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
+ EventBus.getInstance().emit('LINK_ACCEPTED', correlationId, message.accessToken, message.refreshToken);
+ break;
- case "GET_PAIRING_ID":
- const pairingId = await this.mockService.mockGetUserPairingId()
- return {
- type: "GET_PAIRING_ID",
- userPairingId: pairingId,
+ case 'VALIDATE_TOKEN':
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_TOKEN_VALIDATED', correlationId, error);
+ return;
}
+ EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
+ EventBus.getInstance().emit('TOKEN_VALIDATED', correlationId, message.isValid);
+ break;
- case "GET_PROCESSES":
- const processes = await this.mockService.mockGetProcesses()
- return {
- type: "PROCESSES_RETRIEVED",
- processes,
+ case 'RENEW_TOKEN':
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_TOKEN_RENEWED', correlationId, error);
+ return;
}
+ EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
+ EventBus.getInstance().emit('TOKEN_RENEWED', correlationId, message.accessToken, message.refreshToken);
+ break;
- case "GET_MY_PROCESSES":
- const myProcesses = await this.mockService.mockGetMyProcesses()
- return {
- type: "GET_MY_PROCESSES",
- myProcesses,
+ case 'GET_PAIRING_ID':
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_PAIRING_ID', correlationId, error);
+ return;
}
+ EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
+ EventBus.getInstance().emit('PAIRING_ID', correlationId, message.userPairingId);
+ break;
- case "RETRIEVE_DATA":
- const processData = await this.mockService.mockGetData(data.processId, data.stateId)
- return {
- type: "DATA_RETRIEVED",
- data: processData,
+ case 'PROCESSES_RETRIEVED': // GET_PROCESSES
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_PROCESSES_RETRIEVED', correlationId, error);
+ return;
}
+ EventBus.getInstance().emit('PROCESSES_RETRIEVED', correlationId, message.processes);
+ break;
- case "CREATE_PROCESS":
- const created = await this.mockService.mockCreateProcess(data.processData, data.privateFields, data.roles)
- return {
- type: "PROCESS_CREATED",
- processCreated: created,
+ case 'GET_MY_PROCESSES':
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_GET_MY_PROCESSES', correlationId, error);
+ return;
}
+ EventBus.getInstance().emit('GET_MY_PROCESSES', correlationId, message.myProcesses);
+ break;
- case "NOTIFY_UPDATE":
- await this.mockService.mockNotifyProcessUpdate(data.processId, data.stateId)
- return {
- type: "UPDATE_NOTIFIED",
+ case 'DATA_RETRIEVED': // RETRIEVE_DATA
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_DATA_RETRIEVED', correlationId, error);
+ return;
}
+ EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
+ EventBus.getInstance().emit('DATA_RETRIEVED', correlationId, message.data);
+ break;
- case "VALIDATE_STATE":
- const validated = await this.mockService.mockValidateState(data.processId, data.stateId)
- return {
- type: "STATE_VALIDATED",
- validatedProcess: validated,
+ case 'PROCESS_CREATED':
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_PROCESS_CREATED', correlationId, error);
+ return;
}
+ EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
+ EventBus.getInstance().emit('PROCESS_CREATED', correlationId, message.processCreated);
+ break;
- case "VALIDATE_TOKEN":
- const isValid = await this.mockService.mockValidateToken()
- return {
- type: "VALIDATE_TOKEN",
- isValid,
+ case 'UPDATE_NOTIFIED':
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_UPDATE_NOTIFIED', correlationId, error);
+ return;
}
-
- case "RENEW_TOKEN":
- const newTokens = await this.mockService.mockRenewToken()
- return {
- type: "RENEW_TOKEN",
- accessToken: newTokens.accessToken,
- refreshToken: newTokens.refreshToken,
+ EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
+ EventBus.getInstance().emit('UPDATE_NOTIFIED', correlationId);
+ break;
+
+ case 'STATE_VALIDATED':
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_STATE_VALIDATED', correlationId, error);
+ return;
}
+ EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
+ EventBus.getInstance().emit('STATE_VALIDATED', correlationId, message.validatedProcess);
+ break;
- default:
- throw new Error(`Mock message type not implemented: ${type}`)
- }
- }
-
- private initMessageListener(messageId: string, resolve: Function, reject: Function, timeoutMs: number): () => void {
- const timeout = setTimeout(() => {
- cleanup()
- reject(new Error(`Timeout: Aucune réponse après ${timeoutMs / 1000} secondes`))
- }, timeoutMs)
-
- const handleMessage = (event: MessageEvent) => {
- console.log(
- "📥 Received message from:",
- event.origin,
- "Type:",
- event.data.type,
- "MessageId:",
- event.data.messageId,
- )
-
- // Accepter les messages de domaines 4NK ou localhost
- const is4NKOrigin =
- event.origin.includes("4nk") ||
- event.origin.includes("localhost") ||
- event.origin.includes("127.0.0.1") ||
- event.origin === "null" // Pour les iframes en mode file://
-
- if (!is4NKOrigin) {
- console.log("🚫 Message ignored - not from 4NK origin:", event.origin)
- return
- }
-
- if (event.data.messageId !== messageId) {
- console.log("🚫 Message ignored - messageId mismatch")
- return
- }
-
- cleanup()
-
- // Gérer les erreurs selon le protocole 4NK
- if (event.data.type === "ERROR") {
- console.error("❌ Received error:", event.data.error)
- reject(new Error(event.data.error || "Unknown error"))
- } else {
- console.log("✅ Message processed successfully:", event.data.type)
- resolve(event.data)
- }
- }
-
- const cleanup = () => {
- clearTimeout(timeout)
- window.removeEventListener("message", handleMessage)
- }
-
- window.addEventListener("message", handleMessage)
- return cleanup
- }
-
- // Méthodes d'authentification
- async isReady(): Promise {
- if (this.isMockMode) {
- console.log("🎭 Mock mode - iframe ready")
- return Promise.resolve()
- }
-
- return new Promise((resolve, reject) => {
- const iframe = IframeReference.getIframe()
-
- if (!iframe || !iframe.contentWindow) {
- reject(new Error("Iframe not available"))
- return
- }
-
- const targetOrigin = this.getOriginFromUrl(this.origin)
- console.log("🔍 Waiting for LISTENING message from iframe...")
-
- // Timeout de 45 secondes pour isReady
- const timeout = setTimeout(() => {
- cleanup()
- reject(new Error("Timeout: L'iframe 4NK n'a pas envoyé le message LISTENING après 45 secondes."))
- }, 45000)
-
- const cleanup = () => {
- clearTimeout(timeout)
- window.removeEventListener("message", handleMessage)
- }
-
- const handleMessage = (event: MessageEvent) => {
- console.log("📥 Ready check - received from:", event.origin, "Type:", event.data.type)
-
- // Accepter les messages de domaines 4NK
- const is4NKOrigin =
- event.origin.includes("4nk") ||
- event.origin.includes("localhost") ||
- event.origin.includes("127.0.0.1") ||
- event.origin === "null"
-
- if (!is4NKOrigin) {
- console.log("🚫 Ready check ignored - not from 4NK origin")
- return
+ case 'PROCESS_UPDATED':
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_PROCESS_UPDATED', correlationId, error);
+ return;
}
+ console.log('PROCESS_UPDATED', message);
+ EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
+ EventBus.getInstance().emit('PROCESS_UPDATED', correlationId, message.updatedProcess);
+ break;
- if (event.data.type === "LISTENING") {
- console.log("✅ Iframe is ready and listening!")
- cleanup()
- resolve()
+ case 'PUBLIC_DATA_DECODED':
+ if (this.errors[correlationId]) {
+ const error = this.errors[correlationId];
+ delete this.errors[correlationId];
+ EventBus.getInstance().emit('ERROR_PUBLIC_DATA_DECODED', correlationId, error);
+ return;
}
- }
-
- window.addEventListener("message", handleMessage)
- })
- }
-
- async requestLink(): Promise {
- console.log("🔐 Requesting authentication link...")
-
- const response = await this.sendMessage("REQUEST_LINK", {}, 60000)
-
- console.log("📥 Received response:", response)
-
- if (response.type === "LINK_ACCEPTED") {
- console.log("✅ Authentication link accepted")
-
- // Vérifier que nous avons bien reçu les tokens
- if (!response.accessToken || !response.refreshToken) {
- throw new Error("Tokens manquants dans la réponse LINK_ACCEPTED")
- }
-
- // Stocker les tokens en sessionStorage selon le protocole
- this.userStore.connect(response.accessToken, response.refreshToken)
-
- console.log("💾 Tokens stored in sessionStorage")
- } else {
- console.error("❌ Authentication failed:", response)
- throw new Error(`Authentication failed: expected LINK_ACCEPTED, got ${response.type}`)
+ EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
+ EventBus.getInstance().emit('PUBLIC_DATA_DECODED', correlationId, message.decodedData);
+ break;
+
+ case 'ERROR':
+ console.error('Error:', message);
+ this.errors[correlationId] = message.error;
+ break;
}
}
-
- async getUserPairingId(): Promise {
- console.log("🆔 Getting user pairing ID...")
- const response = await this.sendMessage("GET_PAIRING_ID", {}, 30000)
-
- if (response.type !== "GET_PAIRING_ID") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- const pairingId = response.userPairingId
- this.userStore.pair(pairingId)
- console.log("✅ User pairing ID retrieved:", pairingId?.slice(0, 8) + "...")
- return pairingId
- }
-
- async validateToken(): Promise {
- try {
- const response = await this.sendMessage(
- "VALIDATE_TOKEN",
- {
- accessToken: this.userStore.getAccessToken(),
- refreshToken: this.userStore.getRefreshToken(),
- },
- 15000,
- )
-
- return response.isValid === true
- } catch {
- return false
- }
- }
-
- async renewToken(): Promise {
- const response = await this.sendMessage(
- "RENEW_TOKEN",
- {
- refreshToken: this.userStore.getRefreshToken(),
- },
- 30000,
- )
-
- if (response.type === "RENEW_TOKEN") {
- this.userStore.connect(response.accessToken, response.refreshToken)
- } else {
- throw new Error("Failed to renew token")
- }
- }
-
- // Méthodes de gestion des process
- async getProcesses(): Promise {
- const response = await this.sendMessage("GET_PROCESSES", {}, 20000)
-
- if (response.type !== "PROCESSES_RETRIEVED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.processes
- }
-
- async getMyProcesses(): Promise {
- const response = await this.sendMessage("GET_MY_PROCESSES", {}, 20000)
-
- if (response.type !== "GET_MY_PROCESSES") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.myProcesses || []
- }
-
- async getData(processId: string, stateId: string): Promise> {
- const response = await this.sendMessage("RETRIEVE_DATA", { processId, stateId }, 20000)
-
- if (response.type !== "DATA_RETRIEVED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.data
- }
-
- async createProfile(profileData: any, privateFields: string[], roles: any): Promise {
- const response = await this.sendMessage(
- "CREATE_PROCESS",
- {
- processData: profileData,
- privateFields,
- roles,
- },
- 30000,
- )
-
- if (response.type !== "PROCESS_CREATED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.processCreated
- }
-
- async createFolder(folderData: any, privateFields: string[], roles: any): Promise {
- const response = await this.sendMessage(
- "CREATE_PROCESS",
- {
- processData: folderData,
- privateFields,
- roles,
- },
- 30000,
- )
-
- if (response.type !== "PROCESS_CREATED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.processCreated
- }
-
- async updateProcess(
- processId: string,
- lastStateId: string,
- newData: any,
- privateFields: string[],
- roles: any,
- ): Promise {
- const response = await this.sendMessage(
- "UPDATE_PROCESS",
- {
- processId,
- newData,
- privateFields,
- roles,
- },
- 30000,
- )
-
- if (response.type !== "PROCESS_UPDATED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.updatedProcess
- }
-
- async notifyProcessUpdate(processId: string, stateId: string): Promise {
- const response = await this.sendMessage("NOTIFY_UPDATE", { processId, stateId }, 15000)
-
- if (response.type !== "UPDATE_NOTIFIED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
- }
-
- async validateState(processId: string, stateId: string): Promise {
- const response = await this.sendMessage("VALIDATE_STATE", { processId, stateId }, 20000)
-
- if (response.type !== "STATE_VALIDATED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.validatedProcess
- }
-
- // Méthodes utilitaires supplémentaires
- async decodePublicData(encodedData: number[]): Promise {
- const response = await this.sendMessage("DECODE_PUBLIC_DATA", { encodedData }, 15000)
-
- if (response.type !== "PUBLIC_DATA_DECODED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.decodedData
- }
-
- async hashValue(commitedIn: string, label: string, fileBlob: any): Promise {
- const response = await this.sendMessage("HASH_VALUE", { commitedIn, label, fileBlob }, 15000)
-
- if (response.type !== "VALUE_HASHED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.hash
- }
-
- async getMerkleProof(processState: any, attributeName: string): Promise {
- const response = await this.sendMessage("GET_MERKLE_PROOF", { processState, attributeName }, 15000)
-
- if (response.type !== "MERKLE_PROOF_RETRIEVED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.proof
- }
-
- async validateMerkleProof(merkleProof: string, documentHash: string): Promise {
- const response = await this.sendMessage("VALIDATE_MERKLE_PROOF", { merkleProof, documentHash }, 15000)
-
- if (response.type !== "MERKLE_PROOF_VALIDATED") {
- throw new Error(`Unexpected response type: ${response.type}`)
- }
-
- return response.isValid
- }
-
- // Méthodes spécifiques au mock
- getMockStats() {
- if (this.isMockMode) {
- return this.mockService.getMockStats()
- }
- return null
- }
-
- getMockRecentActivity() {
- if (this.isMockMode) {
- return this.mockService.getMockRecentActivity()
- }
- return null
- }
}
diff --git a/lib/4nk/UserStore.ts b/lib/4nk/UserStore.ts
index 3efceb8..76136dd 100644
--- a/lib/4nk/UserStore.ts
+++ b/lib/4nk/UserStore.ts
@@ -1,59 +1,43 @@
-/**
- * UserStore - Singleton pour la gestion des tokens et identifiants utilisateur
- * Stockage en sessionStorage selon les spécifications 4NK
- */
-export class UserStore {
- private static instance: UserStore | null = null
+export default class UserStore {
+ private static instance: UserStore;
- private constructor() {}
+ private constructor() { }
- static getInstance(): UserStore {
+ public static getInstance(): UserStore {
if (!UserStore.instance) {
- UserStore.instance = new UserStore()
+ UserStore.instance = new UserStore();
}
- return UserStore.instance
+ return UserStore.instance;
}
- connect(accessToken: string, refreshToken: string): void {
- if (typeof window !== "undefined") {
- sessionStorage.setItem("4nk_access_token", accessToken)
- sessionStorage.setItem("4nk_refresh_token", refreshToken)
- }
+ public connect(accessToken: string, refreshToken: string): void {
+ sessionStorage.setItem('accessToken', accessToken);
+ sessionStorage.setItem('refreshToken', refreshToken);
}
- isConnected(): boolean {
- if (typeof window === "undefined") return false
- const accessToken = sessionStorage.getItem("4nk_access_token")
- const refreshToken = sessionStorage.getItem("4nk_refresh_token")
- return !!(accessToken && refreshToken)
+ public isConnected(): boolean {
+ return sessionStorage.getItem('accessToken') !== null && sessionStorage.getItem('refreshToken') !== null;
}
- disconnect(): void {
- if (typeof window !== "undefined") {
- sessionStorage.removeItem("4nk_access_token")
- sessionStorage.removeItem("4nk_refresh_token")
- sessionStorage.removeItem("4nk_user_pairing_id")
- }
+ public disconnect(): void {
+ sessionStorage.removeItem('accessToken');
+ sessionStorage.removeItem('refreshToken');
+ sessionStorage.removeItem('userPairingId');
}
- getAccessToken(): string | null {
- if (typeof window === "undefined") return null
- return sessionStorage.getItem("4nk_access_token")
+ public getAccessToken(): string | null {
+ return sessionStorage.getItem('accessToken');
}
- getRefreshToken(): string | null {
- if (typeof window === "undefined") return null
- return sessionStorage.getItem("4nk_refresh_token")
+ public getRefreshToken(): string | null {
+ return sessionStorage.getItem('refreshToken');
}
- pair(userPairingId: string): void {
- if (typeof window !== "undefined") {
- sessionStorage.setItem("4nk_user_pairing_id", userPairingId)
- }
+ public pair(userPairingId: string): void {
+ sessionStorage.setItem('userPairingId', userPairingId);
}
- getUserPairingId(): string | null {
- if (typeof window === "undefined") return null
- return sessionStorage.getItem("4nk_user_pairing_id")
+ public getUserPairingId(): string | null {
+ return sessionStorage.getItem('userPairingId');
}
}