Compare commits

...

35 Commits
master ... dev

Author SHA1 Message Date
4NK Dev
16383491b4 ci: docker_tag=dev-test - Centralisation des fichiers CODE_OF_CONDUCT, CODEOWNERS, CONTRIBUTING, LICENSE 2025-10-01 21:05:53 +00:00
4NK Dev
e86f687f6c ci: docker_tag=dev-test - Mise à jour documentation et standardisation 2025-10-01 20:57:45 +00:00
Sosthene
68f6b31a6c Add getPublicData 2025-06-24 15:53:26 +02:00
omaroughriss
4740f4a67c Add all fields in state render 2025-06-19 20:59:35 +02:00
Sosthene
ae64b057a6 Update fields 2025-06-17 17:10:50 +02:00
Sosthene
1dee8b9ef6 Display private data 2025-06-17 15:54:40 +02:00
Sosthene
334f548cbb [bug] Incorrect formatting of files in ProfileModal 2025-06-15 22:58:55 +02:00
Sosthene
1d0d0c034d pass setProcesses as prop for ProcessesViewer 2025-06-15 22:34:24 +02:00
Sosthene
f404054438 Add process udpate in MessageBus 2025-06-15 22:27:52 +02:00
Sosthene
94b6e58cd0 Display private data for each process 2025-06-15 10:37:08 +02:00
Sosthene
32d76e2328 Update getData 2025-06-15 10:35:24 +02:00
Sosthene
32b5f697c5 Rm dead code 2025-06-15 10:34:55 +02:00
Sosthene
ac68c1d4b1 make iframeUrl export 2025-06-15 10:34:20 +02:00
Sosthene
e6b9f58cea Update handleProfileSubmit 2025-06-13 20:27:08 +02:00
Sosthene
8636d37d29 Update ProfileModal 2025-06-13 20:26:50 +02:00
Sosthene
a25e1b4ae5 Add error-message css 2025-06-13 20:26:26 +02:00
Sosthene
7807ae315f Make createProfile compatible with CREATE_PROCESS interface 2025-06-13 20:25:27 +02:00
Sosthene
2084b99978 Add demiurge role to FolderRoles 2025-06-13 20:24:17 +02:00
Sosthene
d5088d9e36 [bug] Fix ProfileData 2025-06-13 20:23:57 +02:00
Sosthene
24a8be727c Update Profile and Folder with FileBlob 2025-06-13 16:13:59 +02:00
Sosthene
f9f9739cbd Add FileBlob type 2025-06-13 16:12:28 +02:00
Sosthene
a71dc88407 Display all commitments for each process 2025-06-13 16:12:13 +02:00
Sosthene
512d981b1b ProcessesViewer display the filtered number of processes 2025-06-13 16:11:48 +02:00
Sosthene
c6f42e893b Create folder with create/notify/validate flow 2025-06-12 17:25:36 +02:00
Sosthene
02a490d3e3 Define models for roles and folder data 2025-06-12 17:24:54 +02:00
Sosthene
32f11a56ef Update package-lock.json 2025-06-11 21:48:48 +02:00
Sosthene
086fa86bbe Getting ProfileCreated and FolderCreated return values 2025-06-11 21:48:34 +02:00
Sosthene
aad1a4bfe2 Add FolderCreated and ProfileCreated models 2025-06-11 21:47:50 +02:00
8eec5b3455 Filter my processes in processes viewer 2025-06-11 20:37:52 +02:00
09e8530c6a Add getMyProcesses logic 2025-06-11 20:37:00 +02:00
Sosthene
74ae167e3c Set userPairingId on connect 2025-06-11 15:16:30 +02:00
Sosthene
90b2ed5e6e Add userPairingId state in App 2025-06-11 15:16:30 +02:00
Sosthene
78c7d009c1 Add userPairingId in UserStore 2025-06-11 15:15:03 +02:00
Sosthene
d9851105ab Remove hardcoded getData 2025-06-11 15:14:06 +02:00
Sosthene
d8105fa3cf Keep the isReady value for multiple calls 2025-06-11 15:13:51 +02:00
14 changed files with 2738 additions and 484 deletions

99
.gitignore vendored
View File

@ -1,24 +1,79 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# 4NK Environment - Git Ignore
# ============================
confs/
# Dossiers de sauvegarde des scripts
**/backup/
**/*backup*
node_modules
dist
dist-ssr
*.local
**/.cargo/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Fichiers temporaires
**/*.tmp*
**/*.temp*
**/*.log*
**/*.pid*
# Fichiers de configuration locale
**/*.env*
**/*.conf*
**/*.yaml*
**/*.yml*
**/*.ini*
**/*.json*
**/*.toml*
**/*.lock*
# Données et logs
**/*.logs*
**/*.data
*.db
*.sqlite
# Certificats et clés
**/*.key
**/*.pem
**/*.crt
**/*.p12
**/*.pfx
ssl/
certs/
# Docker
**/*.docker*
# Cache et build
**/*.node_modules/
**/*.dist/
**/*build/
**/*target/
**/*.*.o
**/*.so
**/*.dylib
# IDE et éditeurs
**/*.vscode/
**/*.idea/
**/*.swp
**/*.swo
**/*~
# OS
**/*.DS_Store
**/*Thumbs.db
**/*tmp*
# Git
**/*.git/
**/*.orig*
# Backup des projets existants
**/*backup*
**/*wallet*
**/*keys*
**/*node_modules*
**/*cursor*
**/*pid*
**/*next*

View File

@ -44,7 +44,17 @@ L'application communique avec la plateforme [4NK] via une iframe et un bus de me
L'URL de l'iframe est définie dans `App.tsx` :
```typescript
const iframeUrl = 'https://dev3.4nkweb.com'
const iframeUrl = '<PUBLIC_BASE_URL>'
```
Pour modifier l'environnement cible, vous devez changer cette URL.
## 📋 Fichiers centralisés
Les fichiers suivants sont centralisés dans le dépôt principal `4NK_env` :
- `CODE_OF_CONDUCT.md` - Code de conduite
- `CODEOWNERS` - Propriétaires du code
- `CONTRIBUTING.md` - Guide de contribution
- `LICENSE` - Licence du projet
Voir : [`4NK_env/CODE_OF_CONDUCT.md`](../../CODE_OF_CONDUCT.md), [`4NK_env/CODEOWNERS`](../../CODEOWNERS), [`4NK_env/CONTRIBUTING.md`](../../CONTRIBUTING.md), [`4NK_env/LICENSE`](../../LICENSE)

1433
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,10 +11,10 @@ import UserStore from './sdk/UserStrore';
import Iframe from './sdk/Iframe'
import BlockchainViewer from './components/ProcessesViewer';
import FolderModal from './components/FolderModal';
import type { ProfileData } from './sdk/models/ProfileData'
import type { FolderData } from './sdk/models/FolderData'
import { ProfilePrivateFields, setDefaultProfileRoles, type ProfileCreated, type ProfileData } from './sdk/models/ProfileData'
import { FolderPrivateFields, setDefaultFolderRoles, type FolderCreated, type FolderData } from './sdk/models/FolderData'
const iframeUrl = 'https://dev3.4nkweb.com'
export const iframeUrl = 'https://dev3.4nkweb.com'
function App() {
const [receivedMessages, setReceivedMessages] = useState<{ timestamp: string; data: any }[]>([])
@ -23,43 +23,51 @@ function App() {
const [showAuthModal, setShowAuthModal] = useState(false)
const [showFolderModal, setShowFolderModal] = useState(false)
const [processes, setProcesses] = useState<any>(null)
const [myProcesses, setMyProcesses] = useState<string[]>([])
const [userPairingId, setUserPairingId] = useState<string | null>(null)
useEffect(() => {
setIsConnected(UserStore.getInstance().isConnected());
});
useEffect(() => {
setUserPairingId(UserStore.getInstance().getUserPairingId());
});
useEffect(() => {
if (isConnected) {
const messageBus = MessageBus.getInstance(iframeUrl);
messageBus.isReady().then(() => {
messageBus.getProcesses().then((processes: any) => {
setProcesses(processes);
for (const key of Object.keys(processes)) {
try {
const process = processes[key];
if (Object.keys(process.states?.[0]?.keys).length === 0) {
continue;
}
console.log(key);
console.log(process);
} catch (error) {
console.error('Failed to retrieve data:', error);
}
}
messageBus.getData('467b005278cf516a42a54ba777fcbab29748072b52c01a988a596662e7b7844a:0', 'ada06b5c6e5add8a281b284a31a258355b33a9f0dbc4a5dcfe77dfd4eb904011').then((data: any) => {
console.log(data);
});
});
});
}
}, [isConnected, iframeUrl]);
useEffect(() => {
if (isConnected && processes !== null) {
const messageBus = MessageBus.getInstance(iframeUrl);
messageBus.isReady().then(() => {
messageBus.getMyProcesses().then((res: string[]) => {
setMyProcesses(res);
})
});
}
}, [isConnected, processes]);
useEffect(() => {
if (isConnected && userPairingId === null) {
const messageBus = MessageBus.getInstance(iframeUrl);
messageBus.isReady().then(() => {
messageBus.getUserPairingId().then((userPairingId: string) => {
UserStore.getInstance().pair(userPairingId);
setUserPairingId(UserStore.getInstance().getUserPairingId());
})
});
}
}, [isConnected, userPairingId, processes]);
// Gestionnaire pour afficher la modale de connexion
const handleLogin = useCallback(() => {
// Afficher la modale de connexion
@ -100,32 +108,52 @@ function App() {
}, []);
// Gestionnaire pour soumettre les données du profil
const handleProfileSubmit = useCallback((profileData: ProfileData) => {
// Ajouter le validator fixe aux données du profil
const completeProfileData = {
...profileData,
validator: '884cb36a346a79af8697559f16940141f068bdf1656f88fa0df0e9ecd7311fb8:0'
};
MessageBus.getInstance(iframeUrl).createProfile(completeProfileData).then((_profileData: ProfileData) => {
MessageBus.getInstance(iframeUrl).getProcesses().then((processes: any) => {
setProcesses(processes);
const handleProfileSubmit = useCallback((profileData: ProfileData, validatorId: string | null, ownerId: string | null) => {
if (userPairingId !== null) {
if (validatorId === null && ownerId === null) {
console.error("No validator or owner ID provided");
return;
}
const messageBus = MessageBus.getInstance(iframeUrl);
if (validatorId === null) {
validatorId = userPairingId;
} else if (ownerId === null) {
ownerId = userPairingId;
}
const roles = setDefaultProfileRoles([ownerId!], validatorId!);
const profilePrivateFields = ProfilePrivateFields;
messageBus.createProfile(profileData, profilePrivateFields, roles).then((_profileCreated: ProfileCreated) => {
messageBus.notifyProcessUpdate(_profileCreated.processId, _profileCreated.process.states[0].state_id).then(() => {
messageBus.validateState(_profileCreated.processId, _profileCreated.process.states[0].state_id).then((_updatedProcess: any) => {
messageBus.getProcesses().then((processes: any) => {
setProcesses(processes);
});
});
})
});
});
setShowProfileModal(false);
}, []);
setShowProfileModal(false);
}
}, [userPairingId]);
// Gestionnaire pour soumettre les données du dossier
const handleFolderSubmit = useCallback((folderData: FolderData) => {
MessageBus.getInstance(iframeUrl).createFolder(folderData).then((_folderData: FolderData) => {
MessageBus.getInstance(iframeUrl).getProcesses().then((processes: any) => {
setProcesses(processes);
if (userPairingId !== null) {
const roles = setDefaultFolderRoles(userPairingId, [], []);
const folderPrivateFields = FolderPrivateFields;
MessageBus.getInstance(iframeUrl).createFolder(folderData, folderPrivateFields, roles).then((_folderCreated: FolderCreated) => {
MessageBus.getInstance(iframeUrl).notifyProcessUpdate(_folderCreated.processId, _folderCreated.process.states[0].state_id).then(() => {
MessageBus.getInstance(iframeUrl).validateState(_folderCreated.processId, _folderCreated.process.states[0].state_id).then((_updatedProcess: any) => {
MessageBus.getInstance(iframeUrl).getProcesses().then((processes: any) => {
setProcesses(processes);
});
});
})
});
});
setShowFolderModal(false);
}, []);
setShowFolderModal(false);
}
}, [userPairingId]);
// Gestionnaire du clic sur le bouton Vider les messages
const handleClearMessages = useCallback(() => {
@ -161,7 +189,7 @@ function App() {
{/* Espace pour contenu supplémentaire à droite */}
<div className="content-area">
{/* Affichage des blocs de la blockchain */}
<BlockchainViewer processes={processes} />
<BlockchainViewer processes={processes} myProcesses={myProcesses} onProcessesUpdate={setProcesses}/>
</div>
</div>
@ -181,7 +209,7 @@ function App() {
<ProfileModal
isOpen={showProfileModal}
onClose={handleCloseProfileModal}
onSubmit={handleProfileSubmit}
onSubmit={(profileData: ProfileData, validatorId: string | null, ownerId: string | null) => handleProfileSubmit(profileData, validatorId, ownerId)}
/>
)}
{showFolderModal && (

View File

@ -171,4 +171,210 @@
word-break: break-all;
margin-bottom: 0.25rem;
font-size: 0.8rem;
}
}
.process-actions {
margin-top: 20px;
padding: 10px;
text-align: center;
}
.update-button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.update-button:hover {
background-color: #45a049;
}
.data-fields-container {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
padding: 1rem;
background-color: rgba(255, 255, 255, 0.02);
border-radius: 8px;
}
.data-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem;
background-color: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
transition: all 0.2s ease;
}
.data-field:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
border-color: rgba(255, 255, 255, 0.2);
}
.data-field-header {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-color);
}
.data-type-icon,
.data-value-icon {
font-size: 1.1rem;
opacity: 0.8;
}
.data-label {
font-weight: 500;
color: var(--text-color);
}
.data-field-content {
display: flex;
align-items: center;
gap: 1rem;
padding-left: 2rem;
color: var(--text-color);
}
.hash-tooltip {
cursor: help;
opacity: 0.7;
transition: opacity 0.2s ease;
color: var(--text-color);
}
.hash-tooltip:hover {
opacity: 1;
}
.download-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: var(--text-color);
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
}
.download-button:hover {
background-color: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
.loading-message,
.no-access-message {
padding: 1rem;
text-align: center;
color: var(--text-color);
background-color: rgba(255, 255, 255, 0.04);
border-radius: 6px;
margin: 1rem 0;
}
.no-access-message {
color: #ff6b6b;
background-color: rgba(255, 107, 107, 0.1);
}
.field-update-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
background: none;
border: none;
border-radius: 4px;
color: var(--text-color-muted);
cursor: pointer;
transition: all 0.2s ease;
margin-left: auto;
font-size: 1rem;
}
.field-update-button:hover {
color: var(--primary-color);
background-color: rgba(var(--primary-color-rgb), 0.1);
}
.field-update-button:active {
transform: scale(0.95);
}
.edit-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
padding: 0.5rem;
background-color: rgba(255, 255, 255, 0.04);
border-radius: 4px;
}
.edit-form input[type="text"],
.edit-form input[type="number"],
.edit-form select,
.edit-form textarea {
width: 100%;
padding: 0.5rem;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: var(--text-color);
font-size: 0.9rem;
}
.edit-form input[type="file"] {
width: 100%;
padding: 0.5rem;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: var(--text-color);
font-size: 0.9rem;
}
.edit-form textarea {
font-family: monospace;
min-height: 100px;
resize: vertical;
}
.edit-form-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
}
.edit-form-actions button {
padding: 0.25rem 0.75rem;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: var(--text-color);
cursor: pointer;
transition: all 0.2s ease;
}
.edit-form-actions button:hover {
background-color: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
.edit-form-actions button:active {
transform: scale(0.95);
}

View File

@ -1,13 +1,14 @@
import { useState, memo } from 'react';
import './ProcessesViewer.css';
import { isFileBlob, type FileBlob } from '../sdk/models/Data';
import MessageBus from '../sdk/MessageBus';
import { iframeUrl } from '../App';
interface BlockState {
commited_in: string;
state_id: string;
public_data: {
memberPublicName?: string | number[];
pairedAddresses?: string[] | number[];
};
pcd_commitment: Record<string, string>;
public_data: Record<string, any>;
// Autres propriétés disponibles si nécessaires
}
@ -21,10 +22,100 @@ interface Processes {
interface ProcessesViewerProps {
processes: Processes | null;
myProcesses: string[];
onProcessesUpdate?: (processes: Processes) => void;
}
function ProcessesViewer({ processes }: ProcessesViewerProps) {
const compareStates = (
currentState: BlockState,
index: number,
previousState?: BlockState,
currentPrivateData?: Record<string, any>,
previousPrivateData?: Record<string, any>
) => {
const result: Record<string, {
value: any,
status: 'unchanged' | 'modified',
hash?: string,
isPrivate: boolean,
stateId: string
}> = {};
// Ajouter toutes les données publiques de l'état actuel
Object.keys(currentState.public_data).forEach(key => {
const currentValue = currentState.public_data[key];
const previousValue = previousState?.public_data[key];
const isModified = index > 0 &&
previousValue !== undefined &&
JSON.stringify(currentValue) !== JSON.stringify(previousValue);
result[key] = {
value: currentValue,
status: isModified ? 'modified' : 'unchanged',
hash: currentState.pcd_commitment[key],
isPrivate: false,
stateId: currentState.state_id
};
});
// Gérer les données privées
if (index === 0) {
// Pour le premier état, on ajoute simplement les données privées actuelles
if (currentPrivateData) {
Object.entries(currentPrivateData).forEach(([key, value]) => {
result[key] = {
value,
status: 'unchanged',
hash: currentState.pcd_commitment[key],
isPrivate: true,
stateId: currentState.state_id
};
});
}
} else if (previousPrivateData) {
// Pour les états suivants, on commence par les données privées de l'état précédent
Object.entries(previousPrivateData).forEach(([key, value]) => {
result[key] = {
value,
status: 'unchanged',
hash: previousState?.pcd_commitment[key],
isPrivate: true,
stateId: previousState!.state_id
};
});
// Puis on met à jour les données privées qui ont changé
if (currentPrivateData) {
Object.entries(currentPrivateData).forEach(([key, value]) => {
result[key] = {
value,
status: 'modified',
hash: currentState.pcd_commitment[key],
isPrivate: true,
stateId: currentState.state_id
};
});
}
}
return result;
};
function ProcessesViewer({ processes, myProcesses, onProcessesUpdate }: ProcessesViewerProps) {
const [expandedBlocks, setExpandedBlocks] = useState<string[]>([]);
const [isFiltered, setIsFiltered] = useState<boolean>(false);
const [privateData, setPrivateData] = useState<Record<string, Record<string, any>>>({});
const [editingField, setEditingField] = useState<{
processId: string;
stateId: string;
key: string;
value: any;
} | null>(null);
const [tempValue, setTempValue] = useState<any>(null);
const handleFilterClick = () => {
setIsFiltered(prev => !prev);
};
// Si pas de données, afficher un message
if (!processes || Object.keys(processes).length === 0) {
@ -44,64 +135,362 @@ function ProcessesViewer({ processes }: ProcessesViewerProps) {
);
};
const formatAddress = (address: string | number[] | undefined): string => {
if (!address) return "Adresse non disponible";
if (Array.isArray(address)) {
// Si c'est un tableau de nombres, on le convertit en chaîne de caractères
try {
// Convertir les codes ASCII en caractères
const chars = address.map(code => String.fromCharCode(Number(code)));
return chars.join('');
} catch (e) {
return "Adresse encodée (format non supporté)";
}
} else if (typeof address === 'string') {
// Si c'est déjà une chaîne, on la retourne telle quelle
return address;
const fetchPrivateData = async (processId: string, stateId: string) => {
if (!expandedBlocks.includes(processId) || !myProcesses.includes(processId)) {
return;
}
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
return "Format d'adresse inconnu";
const data = await messageBus.getData(processId, stateId);
setPrivateData(prev => ({
...prev,
[stateId]: data
}));
} catch (error) {
console.error('Error fetching private data:', error);
}
};
const formatName = (name: string | number[] | undefined): string => {
if (!name) return "Nom non disponible";
const handleDownload = (name: string | undefined, fileBlob: FileBlob) => {
const blob = new Blob([fileBlob.data], { type: fileBlob.type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name || 'download';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
if (Array.isArray(name)) {
if (name.length === 1 && name[0] === 96) {
return "`"; // Caractère spécial
const formatValue = (key: string, value: string | number[] | FileBlob) => {
if (isFileBlob(value)) {
return (
<button
className="download-button"
onClick={() => handleDownload(key, value)}
title="Télécharger le fichier"
>
📥 Télécharger
</button>
);
}
return JSON.stringify(value || '');
};
const getDataIcon = (value: any) => {
if (isFileBlob(value)) return '📄';
if (typeof value === 'string') return '📝';
if (typeof value === 'number') return '🔢';
if (Array.isArray(value)) return '📋';
if (typeof value === 'boolean') return '✅';
return '📦'; // object
};
const handleFieldUpdate = async (processId: string, stateId: string, key: string, value: any) => {
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
const updateData = {
[key]: value
};
// First update the process
const updatedProcess = await messageBus.updateProcess(processId, stateId, updateData, [], null);
if (!updatedProcess) {
throw new Error('No updated process found');
}
try {
const chars = name.map(code => String.fromCharCode(Number(code)));
return chars.join('');
} catch (e) {
return "Nom encodé (format non supporté)";
const newStateId = updatedProcess.diffs[0]?.state_id;
if (!newStateId) {
throw new Error('No new state id found');
}
} else if (typeof name === 'string') {
return name;
// Then notify about the update
await messageBus.notifyProcessUpdate(processId, newStateId);
// Finally validate the state
await messageBus.validateState(processId, newStateId);
// Refresh the processes data
const updatedProcesses = await messageBus.getProcesses();
if (onProcessesUpdate) {
onProcessesUpdate(updatedProcesses);
}
console.log('Process updated successfully');
} catch (error) {
console.error('Error updating field:', error);
// You might want to show an error message to the user here
}
};
const renderEditForm = (key: string, value: any, onSave: (newValue: any) => void, onCancel: () => void) => {
// Initialize tempValue when editing starts
if (tempValue === null) {
setTempValue(value);
}
return "Format de nom inconnu";
const handleFormClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
};
if (isFileBlob(value)) {
return (
<div className="edit-form">
<input
type="file"
onChange={(e) => {
e.stopPropagation();
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
if (event.target?.result) {
const arrayBuffer = event.target.result as ArrayBuffer;
const uint8Array = new Uint8Array(arrayBuffer);
setTempValue({
type: file.type,
data: uint8Array
});
}
};
reader.readAsArrayBuffer(file);
}
}}
/>
<div className="edit-form-actions">
<button onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onSave(tempValue);
setTempValue(null);
}}>Sauvegarder</button>
<button onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onCancel();
setTempValue(null);
}}>Annuler</button>
</div>
</div>
);
}
if (typeof value === 'boolean') {
return (
<div className="edit-form" onClick={handleFormClick} onMouseDown={handleFormClick}>
<select
value={tempValue.toString()}
onChange={(e) => {
e.stopPropagation();
setTempValue(e.target.value === 'true');
}}
>
<option value="true">Vrai</option>
<option value="false">Faux</option>
</select>
<div className="edit-form-actions">
<button onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onSave(tempValue);
}}>Sauvegarder</button>
<button onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onCancel();
}}>Annuler</button>
</div>
</div>
);
}
if (Array.isArray(value)) {
return (
<div className="edit-form" onClick={handleFormClick} onMouseDown={handleFormClick}>
<textarea
value={JSON.stringify(tempValue, null, 2)}
onChange={(e) => {
e.stopPropagation();
try {
const newValue = JSON.parse(e.target.value);
if (Array.isArray(newValue)) {
setTempValue(newValue);
}
} catch (error) {
// Invalid JSON, ignore
}
}}
onClick={handleFormClick}
onMouseDown={handleFormClick}
rows={4}
/>
<div className="edit-form-actions">
<button onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onSave(tempValue);
}}>Sauvegarder</button>
<button onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onCancel();
}}>Annuler</button>
</div>
</div>
);
}
return (
<div className="edit-form" onClick={handleFormClick} onMouseDown={handleFormClick}>
<input
type={typeof value === 'number' ? 'number' : 'text'}
value={tempValue}
onChange={(e) => {
e.stopPropagation();
const newValue = typeof value === 'number'
? parseFloat(e.target.value)
: e.target.value;
setTempValue(newValue);
}}
onClick={handleFormClick}
onMouseDown={handleFormClick}
autoFocus
/>
<div className="edit-form-actions">
<button onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onSave(tempValue);
}}>Sauvegarder</button>
<button onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onCancel();
}}>Annuler</button>
</div>
</div>
);
};
const renderDataField = (
key: string,
value: any,
hash: string | undefined,
isPrivate: boolean,
processId: string,
stateId: string,
status: 'unchanged' | 'modified' = 'unchanged',
originStateId?: string
) => {
const isEditing = editingField?.key === key &&
editingField?.processId === processId &&
editingField?.stateId === stateId;
const getStatusColor = (status: string) => {
return status === 'modified' ? '#32a852' : 'transparent';
};
return (
<div
className="data-field"
key={key}
style={{
backgroundColor: getStatusColor(status),
transition: 'background-color 0.3s'
}}
>
<div className="data-field-header">
<span className="data-type-icon" title={isPrivate ? 'Donnée privée' : 'Donnée publique'}>
{isPrivate ? '🔒' : '🌐'}
</span>
<span className="data-value-icon">{getDataIcon(value)}</span>
<span className="data-label">{key}</span>
{originStateId && originStateId !== stateId && (
<span className="data-origin" title={`Propagé depuis l'état ${originStateId}`}>
</span>
)}
<button
className="field-update-button"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setEditingField({ processId, stateId, key, value });
}}
title="Mettre à jour cette valeur"
>
{isEditing ? '✕' : '🔄'}
</button>
</div>
<div
className="data-field-content"
>
{isEditing ? (
<div>
{renderEditForm(
key,
value,
async (newValue) => {
await handleFieldUpdate(processId, stateId, key, newValue);
setEditingField(null);
setTempValue(null);
},
() => {
setEditingField(null);
setTempValue(null);
}
)}
</div>
) : (
<>
{formatValue(key, value)}
{hash && (
<div className="hash-tooltip" title={`Hash: ${hash}`}>
🔑
</div>
)}
</>
)}
</div>
</div>
);
};
return (
<div className="processes-viewer">
<h2>Processus</h2>
<p className="block-count">{Object.keys(processes).length} processus disponible(s)</p>
<button onClick={handleFilterClick}>
{isFiltered ? 'Show All Processes' : 'Filter Processes'}
</button>
<p className="block-count">
{isFiltered
? Object.keys(processes).filter(processId => myProcesses.includes(processId)).length
: Object.keys(processes).length} processus disponible(s)
</p>
<div className="block-list">
{Object.entries(processes).map(([blockId, block]) => {
const isExpanded = expandedBlocks.includes(blockId);
const stateCount = block.states.length;
{Object.entries(processes).map(([processId, process]) => {
if (isFiltered && !myProcesses.includes(processId)) {
return null;
}
const isExpanded = expandedBlocks.includes(processId);
const stateCount = process.states.length - 1; // We just ignore the last state, which is always empty
// Le premier état est le plus récent
return (
<div key={blockId} className="block-item">
<div key={processId} className="block-item">
<div
className={`block-header ${isExpanded ? 'expanded' : ''}`}
onClick={() => toggleBlock(blockId)}
onClick={() => toggleBlock(processId)}
>
<div className="block-id">{blockId.substring(0, 8)}...{blockId.substring(blockId.length - 4)}</div>
<div className="block-id">{processId.substring(0, 8)}...{processId.substring(processId.length - 4)}</div>
<div className="block-state-count">{stateCount} état(s)</div>
<div className="block-toggle">{isExpanded ? '▼' : '▶'}</div>
</div>
@ -109,45 +498,62 @@ function ProcessesViewer({ processes }: ProcessesViewerProps) {
{isExpanded && (
<div className="block-details">
<div className="block-complete-id">
<strong>ID complet:</strong> {blockId}
<strong>Process ID:</strong> {processId}
</div>
{block.states.map((state, index) => (
<div key={`${blockId}-state-${index}`} className="state-item">
<h4>État {index + 1}</h4>
<div className="state-detail">
<strong>State ID:</strong> {state.state_id}
</div>
<div className="state-detail">
<strong>Commited dans:</strong> {state.commited_in}
</div>
{process.states.map((state, index) => {
if (index === stateCount) return null;
// Fetch private data if needed
if (myProcesses.includes(processId) && !privateData[state.state_id]) {
console.log('Fetching private data for state:', state.state_id);
setTimeout(() => {
fetchPrivateData(processId, state.state_id);
}, 0);
}
<div className="state-public-data">
<h5>Données publiques</h5>
<div className="public-data-item">
<strong>Nom:</strong> {formatName(state.public_data.memberPublicName)}
const statePrivateData = privateData[state.state_id] || {};
// On utilise compareStates avec l'état précédent et les données privées
const stateData = compareStates(
state,
index,
index > 0 ? process.states[index - 1] : undefined,
statePrivateData,
index > 0 ? privateData[process.states[index - 1].state_id] : undefined
);
return (
<div key={`${processId}-state-${index}`} className="state-item">
<h4>État {index + 1}</h4>
<div className="state-detail">
<strong>TransactionId:</strong> {state.commited_in}
</div>
<div className="state-detail">
<strong>Empreinte totale de l'état:</strong> {state.state_id}
</div>
<div className="data-fields-container">
{/* Toutes les données (publiques et privées) */}
{Object.entries(stateData).map(([key, { value, status, hash, isPrivate, stateId }]) =>
renderDataField(key, value, hash, isPrivate, processId, stateId, status, state.state_id)
)}
{/* Message de chargement pour les données privées si nécessaire */}
{myProcesses.includes(processId) && Object.keys(statePrivateData).length === 0 && (
<div className="loading-message">Chargement des données privées...</div>
)}
{/* Message si pas d'accès aux données privées */}
{!myProcesses.includes(processId) && (
<div className="no-access-message">
🔒 Vous n'avez pas accès aux données privées de ce processus
</div>
)}
</div>
{state.public_data.pairedAddresses && (
<div className="public-data-item">
<strong>Adresses associées:</strong>
<ul className="address-list">
{Array.isArray(state.public_data.pairedAddresses) ?
(typeof state.public_data.pairedAddresses[0] === 'string' ? (
(state.public_data.pairedAddresses as string[]).map((addr, i) => (
<li key={i}>{addr}</li>
))
) : (
<li>{formatAddress(state.public_data.pairedAddresses as number[])}</li>
)) : (
<li>{String(state.public_data.pairedAddresses || '')}</li>
)
}
</ul>
</div>
)}
</div>
</div>
))}
);
})}
</div>
)}
</div>
@ -156,7 +562,7 @@ function ProcessesViewer({ processes }: ProcessesViewerProps) {
</div>
</div>
);
};
}
ProcessesViewer.displayName = 'ProcessesViewer';
export default memo(ProcessesViewer);

View File

@ -165,10 +165,30 @@
box-shadow: 0 2px 4px rgba(103, 58, 183, 0.2);
}
.error-message {
color: red;
background-color: #ffebee; /* Light red background */
padding: 10px;
border-radius: 4px;
margin-top: 10px;
border-left: 4px solid red; /* Left border for emphasis */
font-weight: bold;
animation: fadeIn 0.5s ease-in-out;
}
.error-message::before {
content: "⚠️ "; /* Unicode for warning emoji */
}
/* Styles adaptatifs pour les écrans plus petits */
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
gap: 0.5rem;
}
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

View File

@ -2,11 +2,12 @@ import React, { useState, memo } from 'react';
import Modal from './modal/Modal';
import './ProfileModal.css';
import type { ProfileData } from '../sdk/models/ProfileData';
import type { FileBlob } from '../sdk/models/Data';
interface ProfileModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (profileData: ProfileData) => void;
onSubmit: (profileData: ProfileData, validatorId: string | null, ownerId: string | null) => void;
initialData?: Partial<ProfileData>;
}
@ -20,20 +21,46 @@ function ProfileModal({ isOpen, onClose, onSubmit, initialData = {} }: ProfileMo
postalCode: initialData.postalCode || '',
city: initialData.city || '',
country: initialData.country || '',
idDocument: initialData.idDocument || '',
idDocument: initialData.idDocument || null,
idCertified: false,
});
const [validatorId, setValidatorId] = useState<string | null>(null);
const [ownerId, setOwnerId] = useState<string | null>(null);
const [isOwnProfile, setIsOwnProfile] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setProfileData(prev => ({
...prev,
[name]: value
}));
const { name, type, files, value } = e.target;
if (type === 'file' && files) {
// Assuming you want to handle a single file
const file = files[0];
file.arrayBuffer().then(arrayBuffer => {
const fileBlob: FileBlob = {
type: file.type,
data: new Uint8Array(arrayBuffer)
};
setProfileData(prev => ({
...prev,
[name]: fileBlob
}));
});
} else {
setProfileData(prev => ({
...prev,
[name]: value
}));
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(profileData);
if (!validatorId && !ownerId) {
setErrorMessage("Please set either a Validator ID or an Owner ID.");
return;
}
setErrorMessage(null);
onSubmit(profileData, validatorId, ownerId);
};
return (
@ -157,16 +184,53 @@ function ProfileModal({ isOpen, onClose, onSubmit, initialData = {} }: ProfileMo
<div className="form-field">
<label htmlFor="idDocument">Document d'identité&nbsp;<span className="optional">(optionnel)</span></label>
<input
type="text"
type="file"
id="idDocument"
name="idDocument"
value={profileData.idDocument || ''}
onChange={handleChange}
placeholder="Numéro de document"
/>
</div>
</div>
<div className="form-section">
<h3 className="section-title">Profil</h3>
<button type="button" onClick={() => setIsOwnProfile(!isOwnProfile)}>
{isOwnProfile ? "Je veux faire valider mon profil" : "Je valide le profil d'un utilisateur"}</button>
{isOwnProfile ? (
<div className="form-field">
<label htmlFor="validatorId">ID du validateur</label>
<input
type="text"
id="validatorId"
name="validatorId"
value={validatorId || ''}
onChange={(e) => {
setValidatorId(e.target.value);
setOwnerId(null);
}}
placeholder="ID du validateur"
/>
</div>
) : (
<div className="form-field">
<label htmlFor="ownerId">ID de l'utilisateur</label>
<input
type="text"
id="ownerId"
name="ownerId"
value={ownerId || ''}
onChange={(e) => {
setOwnerId(e.target.value);
setValidatorId(null);
}}
placeholder="ID de l'utilisateur"
/>
</div>
)}
</div>
{errorMessage && <div className="error-message">{errorMessage}</div>}
<div className="form-actions">
<button type="button" className="btn-cancel" onClick={onClose}>Annuler</button>
<button type="submit" className="btn-submit">Créer le profil</button>

View File

@ -1,15 +1,18 @@
import IframeReference from './IframeReference';
import EventBus from './EventBus';
import UserStore from './UserStrore';
import type { ProfileData } from './models/ProfileData';
import type { FolderData } from './models/FolderData';
import { isProfileData, type ProfileCreated, type ProfileData } from './models/ProfileData';
import { isFolderData, type FolderCreated, type FolderData } from './models/FolderData';
import { v4 as uuidv4 } from 'uuid';
import type { RoleDefinition } from './models/Roles';
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<void> | null = null;
private isReadyFlag = false;
private constructor(origin: string) {
this.origin = origin;
@ -23,19 +26,28 @@ export default class MessageBus {
}
public isReady(): Promise<void> {
return new Promise<void>((resolve: () => void) => {
if (this.isReadyFlag) {
return Promise.resolve();
}
if (this.readyPromise) {
return this.readyPromise;
}
this.readyPromise = new Promise<void>((resolve) => {
const correlationId = uuidv4();
this.initMessageListener(correlationId);
const unsubscribe = EventBus.getInstance().on('IS_READY', (responseId: string) => {
if (responseId !== correlationId) {
return;
}
if (responseId !== correlationId) return;
unsubscribe();
this.destroyMessageListener();
this.isReadyFlag = true;
resolve();
});
});
return this.readyPromise;
}
public requestLink(): Promise<void> {
@ -68,6 +80,41 @@ export default class MessageBus {
});
}
public getUserPairingId(): Promise<string> {
return new Promise<string>((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<boolean> {
return new Promise<boolean>((resolve: (isValid: boolean) => void, reject: (error: string) => void) => {
const userStore = UserStore.getInstance();
@ -148,10 +195,15 @@ export default class MessageBus {
public getProcesses(): Promise<any> {
return new Promise<any>((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;
}
@ -170,14 +222,15 @@ export default class MessageBus {
});
this.sendMessage({
type: 'GET_PROCESSES'
type: 'GET_PROCESSES',
accessToken,
});
}).catch(console.error);
});
}
public getData(processId: string, stateId: string): Promise<any> {
return new Promise<any>((resolve: (data: any) => void, reject: (error: string) => void) => {
public getMyProcesses(): Promise<string[]> {
return new Promise<string[]>((resolve: (myProcesses: string[]) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const userStore = UserStore.getInstance();
const accessToken = userStore.getAccessToken()!;
@ -185,7 +238,42 @@ export default class MessageBus {
const correlationId = uuidv4();
this.initMessageListener(correlationId);
const unsubscribe = EventBus.getInstance().on('DATA_RETRIEVED', (responseId: string, data: any) => {
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<Record<string, any>> {
return new Promise<Record<string, any>>((resolve: (data: Record<string, 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('DATA_RETRIEVED', (responseId: string, data: Record<string, any>) => {
if (responseId !== correlationId) {
return;
}
@ -207,32 +295,31 @@ export default class MessageBus {
type: 'RETRIEVE_DATA',
processId,
stateId,
token: accessToken
accessToken
});
}).catch(console.error);
});
}
public createProfile(profileData: ProfileData): Promise<ProfileData> {
return new Promise<ProfileData>((resolve: (profileData: ProfileData) => void, reject: (error: string) => void) => {
public getPublicData(encodedData: number[]): Promise<any> {
return new Promise<any>((resolve: (data: any) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const userStore = UserStore.getInstance();
const accessToken = userStore.getAccessToken()!;
const refreshToken = userStore.getRefreshToken()!;
const correlationId = uuidv4();
this.initMessageListener(correlationId);
const unsubscribe = EventBus.getInstance().on('PROFILE_CREATED', (responseId: string, profileData: ProfileData) => {
const unsubscribe = EventBus.getInstance().on('PUBLIC_DATA_DECODED', (responseId: string, data: any) => {
if (responseId !== correlationId) {
return;
}
unsubscribe();
this.destroyMessageListener();
resolve(profileData);
resolve(data);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_PROFILE_CREATED', (responseId: string, error: string) => {
const unsubscribeError = EventBus.getInstance().on('ERROR_PUBLIC_DATA_DECODED', (responseId: string, error: string) => {
if (responseId !== correlationId) {
return;
}
@ -242,17 +329,16 @@ export default class MessageBus {
});
this.sendMessage({
type: 'CREATE_PROFILE',
profileData,
accessToken,
refreshToken
type: 'DECODE_PUBLIC_DATA',
encodedData,
accessToken
});
}).catch(console.error);
});
}
public createFolder(folderData: FolderData): Promise<FolderData> {
return new Promise<FolderData>((resolve: (folderData: FolderData) => void, reject: (error: string) => void) => {
public createProfile(profileData: ProfileData, profilePrivateData: string[], roles: Record<string, RoleDefinition>): Promise<ProfileCreated> {
return new Promise<ProfileCreated>((resolve: (profileCreated: ProfileCreated) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const userStore = UserStore.getInstance();
const accessToken = userStore.getAccessToken()!;
@ -260,16 +346,33 @@ export default class MessageBus {
const correlationId = uuidv4();
this.initMessageListener(correlationId);
const unsubscribe = EventBus.getInstance().on('FOLDER_CREATED', (responseId: string, folderData: FolderData) => {
const unsubscribe = EventBus.getInstance().on('PROCESS_CREATED', (responseId: string, processCreated: any) => {
if (responseId !== correlationId) {
return;
}
unsubscribe();
this.destroyMessageListener();
resolve(folderData);
// Return value must contain the data commited in the new process
const profileData = processCreated.processData;
if (!profileData || !isProfileData(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: ProfileCreated = {
processId: processCreated.processId,
process: processCreated.process,
profileData
};
resolve(profileCreated);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_FOLDER_CREATED', (responseId: string, error: string) => {
const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_CREATED', (responseId: string, error: string) => {
if (responseId !== correlationId) {
return;
}
@ -279,9 +382,177 @@ export default class MessageBus {
});
this.sendMessage({
type: 'CREATE_FOLDER',
folderData,
token: accessToken
type: 'CREATE_PROCESS',
processData: profileData,
privateFields: profilePrivateData,
roles,
accessToken
});
}).catch(console.error);
});
}
public createFolder(folderData: FolderData, folderPrivateData: string[], roles: Record<string, RoleDefinition>): Promise<FolderCreated> {
return new Promise<FolderCreated>((resolve: (folderData: FolderCreated) => 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 || !isFolderData(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: 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<string, any>, privateFields: string[], roles: Record<string, RoleDefinition> | null): Promise<any> {
return new Promise<any>((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<void> {
return new Promise<void>((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<any> {
return new Promise<any>((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);
});
@ -371,6 +642,17 @@ export default class MessageBus {
EventBus.getInstance().emit('TOKEN_RENEWED', correlationId, message.accessToken, message.refreshToken);
break;
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 'PROCESSES_RETRIEVED': // GET_PROCESSES
if (this.errors[correlationId]) {
const error = this.errors[correlationId];
@ -381,25 +663,14 @@ export default class MessageBus {
EventBus.getInstance().emit('PROCESSES_RETRIEVED', correlationId, message.processes);
break;
case 'PROFILE_CREATED': // CREATE_PROFILE
case 'GET_MY_PROCESSES':
if (this.errors[correlationId]) {
const error = this.errors[correlationId];
delete this.errors[correlationId];
EventBus.getInstance().emit('ERROR_PROFILE_CREATED', correlationId, error);
EventBus.getInstance().emit('ERROR_GET_MY_PROCESSES', correlationId, error);
return;
}
EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
EventBus.getInstance().emit('PROFILE_CREATED', correlationId, message.profileData);
break;
case 'FOLDER_CREATED': // CREATE_FOLDER
if (this.errors[correlationId]) {
const error = this.errors[correlationId];
delete this.errors[correlationId];
EventBus.getInstance().emit('ERROR_FOLDER_CREATED', correlationId, error);
return;
}
EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
EventBus.getInstance().emit('GET_MY_PROCESSES', correlationId, message.myProcesses);
break;
case 'DATA_RETRIEVED': // RETRIEVE_DATA
@ -413,6 +684,62 @@ export default class MessageBus {
EventBus.getInstance().emit('DATA_RETRIEVED', correlationId, message.data);
break;
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 'UPDATE_NOTIFIED':
if (this.errors[correlationId]) {
const error = this.errors[correlationId];
delete this.errors[correlationId];
EventBus.getInstance().emit('ERROR_UPDATE_NOTIFIED', correlationId, error);
return;
}
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;
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;
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;
}
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;

View File

@ -22,6 +22,7 @@ export default class UserStore {
public disconnect(): void {
sessionStorage.removeItem('accessToken');
sessionStorage.removeItem('refreshToken');
sessionStorage.removeItem('userPairingId');
}
public getAccessToken(): string | null {
@ -31,4 +32,12 @@ export default class UserStore {
public getRefreshToken(): string | null {
return sessionStorage.getItem('refreshToken');
}
public pair(userPairingId: string): void {
sessionStorage.setItem('userPairingId', userPairingId);
}
public getUserPairingId(): string | null {
return sessionStorage.getItem('userPairingId');
}
}

15
src/sdk/models/Data.ts Normal file
View File

@ -0,0 +1,15 @@
export interface FileBlob {
type: string,
data: Uint8Array
};
export function isFileBlob(data: any): data is FileBlob {
return (
typeof data === 'object' &&
data !== null &&
'type' in data &&
typeof data.type === 'string' &&
'data' in data &&
data.data instanceof Uint8Array
);
}

View File

@ -1,3 +1,6 @@
import { isFileBlob, type FileBlob } from "./Data";
import type { RoleDefinition } from "./Roles";
export interface FolderData {
folderNumber: string;
name: string;
@ -8,7 +11,127 @@ export interface FolderData {
created_at: string;
updated_at: string;
customers: string[];
documents: string[];
documents: FileBlob[];
motes: string[];
stakeholders: string[];
}
export function isFolderData(data: any): data is FolderData {
if (typeof data !== 'object' || data === null) return false;
const requiredStringFields = [
'folderNumber',
'name',
'deedType',
'description',
'archived_description',
'status',
'created_at',
'updated_at'
];
for (const field of requiredStringFields) {
if (typeof data[field] !== 'string') return false;
}
const requiredArrayFields = [
'customers',
'motes',
'stakeholders'
];
for (const field of requiredArrayFields) {
if (!Array.isArray(data[field]) || !data[field].every((item: any) => typeof item === 'string')) {
return false;
}
}
const requiredFileBlobArrayFields = [
'documents',
];
for (const field of requiredFileBlobArrayFields) {
if (!Array.isArray(data[field])) return false;
if (data[field].length > 0 && !data[field].every(isFileBlob)) return false;
}
return true;
}
const emptyFolderData: FolderData = {
folderNumber: '',
name: '',
deedType: '',
description: '',
archived_description: '',
status: '',
created_at: '',
updated_at: '',
customers: [],
documents: [],
motes: [],
stakeholders: []
};
const folderDataFields: string[] = Object.keys(emptyFolderData);
const FolderPublicFields: string[] = [];
// All the attributes are private in that case
export const FolderPrivateFields = [
...folderDataFields.filter(key => !FolderPublicFields.includes(key))
];
export interface FolderCreated {
processId: string,
process: any, // Process
folderData: FolderData,
}
export function setDefaultFolderRoles(ownerId: string, stakeholdersId: string[], customersId: string[]): Record<string, RoleDefinition> {
return {
demiurge: {
members: [ownerId],
validation_rules: [],
storages: []
},
owner: {
members: [ownerId],
validation_rules: [
{
quorum: 0.5,
fields: [...folderDataFields, 'roles'],
min_sig_member: 1,
},
],
storages: []
},
stakeholders: {
members: stakeholdersId,
validation_rules: [
{
quorum: 0.5,
fields: ['documents', 'motes'],
min_sig_member: 1,
},
],
storages: []
},
customers: {
members: customersId,
validation_rules: [
{
quorum: 0.0,
fields: folderDataFields,
min_sig_member: 0.0,
},
],
storages: []
},
apophis: {
members: [ownerId],
validation_rules: [],
storages: []
}
}
};

View File

@ -1,3 +1,6 @@
import { isFileBlob, type FileBlob } from "./Data";
import type { RoleDefinition } from "./Roles";
export interface ProfileData {
name: string;
surname: string;
@ -7,5 +10,113 @@ export interface ProfileData {
postalCode: string;
city: string;
country: string;
idDocument?: string;
idDocument: FileBlob | null;
idCertified: boolean;
}
export function isProfileData(data: any): data is ProfileData{
if (typeof data !== 'object' || data === null) return false;
const requiredStringFields = [
'name',
'surname',
'email',
'phone',
'address',
'postalCode',
'city',
'country',
];
for (const field of requiredStringFields) {
if (typeof data[field] !== 'string') return false;
}
const requiredBooleanFields = [
'idCertified',
];
for (const field of requiredBooleanFields) {
if (typeof data[field] !== 'boolean') return false;
}
const requiredFileFields = [
'idDocument',
];
for (const field of requiredFileFields) {
if (!isFileBlob(data[field]) && data[field] !== null) return false;
}
return true;
}
const emptyProfileData: ProfileData = {
name: '',
surname: '',
email: '',
phone: '',
address: '',
postalCode: '',
city: '',
country: '',
idDocument: null,
idCertified: false,
};
const profileDataFields: string[] = Object.keys(emptyProfileData);
const ProfilePublicFields: string[] = ['idCertified'];
export const ProfilePrivateFields = [
...profileDataFields.filter(key => !ProfilePublicFields.includes(key))
];
export interface ProfileCreated {
processId: string,
process: any, // Process
profileData: ProfileData,
}
export function setDefaultProfileRoles(ownerId: string[], validatorId: string): Record<string, RoleDefinition> {
return {
demiurge: {
members: [...ownerId, validatorId],
validation_rules: [],
storages: []
},
owner: {
members: ownerId,
validation_rules: [
{
quorum: 0.5,
fields: [...ProfilePrivateFields, 'roles'],
min_sig_member: 1,
},
],
storages: []
},
validator: {
members: [validatorId],
validation_rules: [
{
quorum: 0.5,
fields: ['idCertified', 'roles'],
min_sig_member: 1,
},
{
quorum: 0.0,
fields: [...profileDataFields],
min_sig_member: 0,
},
],
storages: []
},
apophis: {
members: ownerId,
validation_rules: [],
storages: []
}
}
};

11
src/sdk/models/Roles.ts Normal file
View File

@ -0,0 +1,11 @@
export interface ValidationRule {
quorum: number,
fields: string[],
min_sig_member: number,
}
export interface RoleDefinition {
members: string[],
validation_rules: ValidationRule[],
storages: string[]
}