diff --git a/frontend/.env.example b/frontend/.env.example index 48650031..7c6a0c46 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -6,6 +6,10 @@ NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= NEXT_PUBLIC_FIREBASE_APP_ID= +# Acta +NEXT_PUBLIC_ACTA_API_URL=https://acta.up.railway.app +NEXT_PUBLIC_ACTA_DEFAULT_NETWORK=testnet + # STELLAR CONTRACT NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015 diff --git a/frontend/src/@types/acta.types.ts b/frontend/src/@types/acta.types.ts new file mode 100644 index 00000000..542d0244 --- /dev/null +++ b/frontend/src/@types/acta.types.ts @@ -0,0 +1,377 @@ +/** + * ACTA (Accountable Credential Transparency and Authentication) Types + * + * Minimal type definitions for ACTA API responses and data structures. + */ + +// ============================================================================ +// BASE RESPONSE TYPES +// ============================================================================ + +/** + * Successful ACTA API response + */ +export interface ActaOk { + success: true; + data: T; +} + +/** + * Error ACTA API response + */ +export interface ActaErr { + success: false; + error: string; +} + +/** + * Union type for all ACTA API responses + */ +export type ActaResponse = ActaOk | ActaErr; + +// ============================================================================ +// HEALTH & STATUS TYPES +// ============================================================================ + +/** + * ACTA health check response data + */ +export interface ActaHealth { + status: "healthy" | "unhealthy" | "degraded"; + timestamp: string; + version?: string; + uptime?: number; + database?: { + status: "connected" | "disconnected"; + latency?: number; + }; + dependencies?: Record< + string, + { + status: "up" | "down"; + latency?: number; + } + >; +} + +// ============================================================================ +// CREDENTIAL TYPES +// ============================================================================ + +/** + * Credential subject information + */ +export interface CredentialSubject { + id: string; + [key: string]: unknown; +} + +/** + * Credential issuer information + */ +export interface CredentialIssuer { + id: string; + name?: string; + [key: string]: unknown; +} + +/** + * Credential status information + */ +export interface CredentialStatus { + id: string; + type: string; + status?: string; +} + +/** + * Credential evidence + */ +export interface CredentialEvidence { + id: string; + type: string; + [key: string]: unknown; +} + +/** + * Create credential request body + */ +export interface CreateCredentialReq { + credentialSubject: CredentialSubject; + issuer: CredentialIssuer; + issuanceDate?: string; + expirationDate?: string; + credentialStatus?: CredentialStatus; + evidence?: CredentialEvidence[]; + [key: string]: unknown; +} + +/** + * Create credential response data + */ +export interface CreateCredentialRes { + id: string; + hash: string; + status: string; + createdAt: string; + updatedAt: string; + credential: CreateCredentialReq; +} + +/** + * Get credential response data + */ +export interface GetCredentialRes { + id: string; + hash: string; + status: string; + createdAt: string; + updatedAt: string; + credential: CreateCredentialReq; + metadata?: { + verified: boolean; + verificationDate?: string; + issuerSignature?: string; + }; +} + +/** + * Update credential status request + */ +export interface UpdateCredentialStatusReq { + status: string; +} + +/** + * Update credential status response data + */ +export interface UpdateCredentialStatusRes { + id: string; + hash: string; + status: string; + updatedAt: string; +} + +// ============================================================================ +// POOL PARTICIPATION TYPES +// ============================================================================ + +/** + * Pool participation duration options + */ +export type PoolDuration = "30d" | "90d" | "180d" | "365d" | "perpetual"; + +/** + * Risk level for pool participation + */ +export type PoolRiskLevel = "low" | "medium" | "high" | "very-high"; + +/** + * Pool participation duration for credentials (Phase 2) + */ +export type ParticipationDuration = "3+ months" | "6+ months" | "12+ months"; + +/** + * Risk level for credentials (Phase 2) + */ +export type CredentialRiskLevel = "Conservative" | "Moderate" | "Aggressive"; + +/** + * Performance tier for credentials (Phase 2) + */ +export type PerformanceTier = + | "No liquidations" + | "Stable participant" + | "Recovered events"; + +/** + * Pool type experience for credentials (Phase 2) + */ +export type PoolTypeExperience = + | "Multi-asset" + | "Stablecoin" + | "LSD" + | "LP-Perp"; + +/** + * Pool participation reputation claims for credential creation + */ +export interface ReputationClaims { + participationDuration: ParticipationDuration; + riskLevel: CredentialRiskLevel; + performanceTier: PerformanceTier; + poolTypeExperience: PoolTypeExperience[]; +} + +/** + * Pool participation credential data structure (Phase 2) + */ +export interface PoolParticipationCredentialData { + type: "PoolParticipationCredential"; + credentialSubject: { + reputationClaims: ReputationClaims; + }; + issuer?: string; + issuanceDate: string; + expirationDate?: string; +} + +/** + * Local credential record for localStorage management + */ +export interface LocalCredentialRecord { + localId: string; + contractId: string; + hash: string; + displayData: { + type: string; + participationDuration: ParticipationDuration; + riskLevel: CredentialRiskLevel; + performanceTier: PerformanceTier; + poolTypeExperience: PoolTypeExperience[]; + issuer?: string; + }; + createdAt: string; +} + +/** + * Pool performance metrics + */ +export interface PoolPerformance { + apy: number; // Annual Percentage Yield + totalValueLocked: number; + utilizationRate: number; + defaultRate?: number; + historicalReturns?: Array<{ + period: string; + return: number; + }>; +} + +/** + * Pool type classification + */ +export type PoolType = + | "lending" + | "borrowing" + | "liquidity" + | "staking" + | "yield-farming" + | "derivatives" + | "insurance"; + +/** + * Pool participation claims for credential generation + */ +export interface PoolParticipationClaims { + // Duration information + duration: PoolDuration; + startDate: string; + endDate?: string; // Optional for perpetual pools + + // Risk assessment + risk: PoolRiskLevel; + riskScore?: number; // 0-100 risk score + riskFactors?: string[]; // Array of risk factor descriptions + + // Performance metrics + performance: PoolPerformance; + expectedReturns?: { + min: number; + max: number; + avg: number; + }; + + // Pool classification + poolType: PoolType; + poolId: string; + poolName?: string; + poolDescription?: string; + + // Participation details + amount: number; + currency: string; + participationDate: string; + + // Additional metadata + terms?: { + minLockPeriod?: string; + penaltyForEarlyWithdrawal?: number; + autoRenewal?: boolean; + }; + + // Verification data + verification?: { + verifiedBy: string; + verificationDate: string; + verificationMethod: string; + [key: string]: unknown; + }; +} + +/** + * Pool participation credential request + */ +export interface CreatePoolParticipationCredentialReq + extends CreateCredentialReq { + credentialSubject: CredentialSubject & { + poolParticipation: PoolParticipationClaims; + }; +} + +/** + * Pool participation credential response + */ +export interface CreatePoolParticipationCredentialRes + extends CreateCredentialRes { + credential: CreatePoolParticipationCredentialReq; +} + +// ============================================================================ +// UTILITY TYPES +// ============================================================================ + +/** + * Check if a response is successful + */ +export function isActaOk(response: ActaResponse): response is ActaOk { + return response.success === true; +} + +/** + * Check if a response is an error + */ +export function isActaErr(response: ActaResponse): response is ActaErr { + return response.success === false; +} + +/** + * Extract data from successful response or throw error + */ +export function extractActaData(response: ActaResponse): T { + if (isActaOk(response)) { + return response.data; + } + throw new Error(response.error); +} + +/** + * Pool duration in days + */ +export const POOL_DURATION_DAYS: Record = { + "30d": 30, + "90d": 90, + "180d": 180, + "365d": 365, + perpetual: Infinity, +} as const; + +/** + * Risk level weights for calculations + */ +export const RISK_LEVEL_WEIGHTS: Record = { + low: 1, + medium: 2, + high: 3, + "very-high": 4, +} as const; diff --git a/frontend/src/app/dev/credential-selector/page.tsx b/frontend/src/app/dev/credential-selector/page.tsx new file mode 100644 index 00000000..560b0b12 --- /dev/null +++ b/frontend/src/app/dev/credential-selector/page.tsx @@ -0,0 +1,253 @@ +"use client"; + +import React, { useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { CredentialSelector } from "@/components/modules/credentials/ui/components/CredentialSelector"; +import { ActaClient } from "@/lib/acta/client"; +import { CredentialWithStatus } from "@/components/modules/credentials/hooks/useCredentials"; +import { toast } from "sonner"; + +/** + * Test page for the credential selector component + */ +export default function CredentialSelectorTestPage() { + const [selectedCredential, setSelectedCredential] = useState< + CredentialWithStatus | undefined + >(); + + // Initialize Acta client (in a real app, this would come from context/config) + const client = new ActaClient({ + baseUrl: process.env.NEXT_PUBLIC_ACTA_API_URL || "http://localhost:3001", + apiKey: process.env.NEXT_PUBLIC_ACTA_API_KEY, + }); + + const handleCredentialSelect = (credential: CredentialWithStatus) => { + setSelectedCredential(credential); + toast.success("Credential selected successfully!"); + }; + + const handleClearSelection = () => { + setSelectedCredential(undefined); + toast.info("Credential selection cleared"); + }; + + const handleProceedWithCredential = () => { + if (selectedCredential) { + toast.success( + `Proceeding with credential: ${selectedCredential.displayData.participationDuration} - ${selectedCredential.displayData.riskLevel}`, + ); + } + }; + + return ( +
+
+ {/* Header */} +
+

+ + Credential Selector Test +

+

+ Test the credential selection component for pool entry flow +

+
+ + {/* Main Test Card */} + + + + Pool Entry with Credential + + + Select a credential to verify your reputation when entering a pool + + + + {/* Credential Selector */} +
+ + +

+ Credentials help verify your experience and may unlock better + terms +

+
+ + {/* Selected Credential Display */} + {selectedCredential && ( + + + + + Selected Credential + + + +
+
+ Duration: + + {selectedCredential.displayData.participationDuration} + +
+
+ Risk Level: + + {selectedCredential.displayData.riskLevel} + +
+
+ Performance: + + {selectedCredential.displayData.performanceTier} + +
+
+ Pool Types: + + {selectedCredential.displayData.poolTypeExperience.join( + ", ", + )} + +
+
+ + {selectedCredential.displayData.issuer && ( +
+ Issuer: + + {selectedCredential.displayData.issuer} + +
+ )} + +
+ Contract: {selectedCredential.contractId.substring(0, 12)} + ... +
+
+
+ )} + + {/* Actions */} +
+ + + {selectedCredential && ( + + )} +
+
+
+ + {/* Information Cards */} +
+ + + + + How It Works + + + +
+ +
+

+ Credential Verification +

+

+ Your credentials are verified against the blockchain +

+
+
+
+ +
+

Privacy-Preserving

+

+ Only necessary reputation claims are shared +

+
+
+
+ +
+

Optional Selection

+

+ You can enter pools with or without credentials +

+
+
+
+
+ + + + + + Benefits + + + +
+ +
+

Better Terms

+

+ Verified users may get better interest rates +

+
+
+
+ +
+

Trust Building

+

+ Establish reputation in the ecosystem +

+
+
+
+ +
+

Access Features

+

Unlock advanced pool features

+
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/app/dev/credential-test/page.tsx b/frontend/src/app/dev/credential-test/page.tsx new file mode 100644 index 00000000..8845a084 --- /dev/null +++ b/frontend/src/app/dev/credential-test/page.tsx @@ -0,0 +1,389 @@ +"use client"; + +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { CreateCredentialModal } from "@/components/modules/credentials/ui/components/CreateCredentialModal"; +import { ActaClient } from "@/lib/acta/client"; +import { + getCredentialsFromLocalStorage, + type LocalCredentialRecord, +} from "@/components/modules/credentials/hooks/useCredentialCreate"; +import { toast } from "sonner"; + +/** + * Test page for credential creation functionality + */ +export default function CredentialTestPage() { + const [isModalOpen, setIsModalOpen] = useState(false); + const [credentials, setCredentials] = useState([]); + + // Initialize Acta client (in a real app, this would come from context/config) + const client = new ActaClient({ + baseUrl: process.env.NEXT_PUBLIC_ACTA_API_URL || "http://localhost:3001", + apiKey: process.env.NEXT_PUBLIC_ACTA_API_KEY, + }); + + // Load credentials from localStorage on component mount + React.useEffect(() => { + const loadedCredentials = getCredentialsFromLocalStorage(); + setCredentials(loadedCredentials); + }, []); + + // This function is passed to the modal but not used directly here + // const handleCredentialCreated = () => { + // // Refresh the credentials list + // const updatedCredentials = getCredentialsFromLocalStorage(); + // setCredentials(updatedCredentials); + // setIsModalOpen(false); + // }; + + const handleClearCredentials = () => { + localStorage.removeItem("tb_vc_index_v1"); + setCredentials([]); + toast.success("Credentials cleared from local storage"); + }; + + const handleTestApiConnection = async () => { + try { + const response = await client.ping(); + if (response.success) { + toast.success("API Connection Successful", { + description: `Response: ${JSON.stringify(response.data)}`, + }); + } else { + toast.error("API Connection Failed", { + description: response.error, + }); + } + } catch (error) { + toast.error("API Connection Error", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } + }; + + return ( +
+
+ {/* Header */} +
+
+ +

+ Credential Creation Test +

+
+

+ Test the Phase 2 credential creation functionality +

+
+ + {/* API Connection Test */} + + +
+ + API Connection Test +
+ + Test connection to the Acta API service + +
+ +
+ +
+ API URL:{" "} + + {process.env.NEXT_PUBLIC_ACTA_API_URL || + "http://localhost:3001"} + +
+
+
+
+ + {/* Credential Creation */} + + +
+ + + Create New Credential + +
+ + Create a pool participation credential with reputation claims. + Credentials are stored on the Stellar blockchain via Acta. + +
+ + + +
+ + {/* Local Storage Management */} + + +
+ + + Local Storage Management + +
+ + Credentials are stored on Stellar blockchain via Acta. This is + just a local index for convenience. + +
+ +
+
+ + +
+
+ Storage Key:{" "} + tb_vc_index_v1 | + Count:{" "} + + {credentials.length} + +
+
+
+
+ + {/* Credentials List */} + {credentials.length > 0 && ( + + +
+ + + Stored Credentials ({credentials.length}) + +
+ + Local index of recently created credentials (real storage is on + Stellar blockchain) + +
+ +
+ {credentials.map((credential) => ( +
+
+
+
+ +

+ {credential.displayData.type} +

+
+

+ + Created:{" "} + {new Date(credential.createdAt).toLocaleString()} +

+
+
+
+ + + Contract:{" "} + {credential.contractId?.substring(0, 8) || "N/A"}... + +
+
+ + + Hash: {credential.hash?.substring(0, 8) || "N/A"}... + +
+
+ + + TX: {credential.hash?.substring(0, 8) || "N/A"}... + +
+
+
+ +
+
+ + + Duration: + {" "} + + {credential.displayData.participationDuration} + +
+
+ + + Risk: + {" "} + + {credential.displayData.riskLevel} + +
+
+ + + Performance: + {" "} + + {credential.displayData.performanceTier} + +
+
+ + + Pool Types: + {" "} + + {credential.displayData.poolTypeExperience.join(", ")} + +
+
+ + {credential.displayData.issuer && ( +
+ + + Issuer: + {" "} + + {credential.displayData.issuer} + +
+ )} +
+ ))} +
+
+
+ )} + + {/* Instructions */} + + +
+ + Test Instructions +
+
+ +
+
+ + 1 + +

+ First, test the API connection to ensure the Acta service is + running +

+
+
+ + 2 + +

+ Click "Open Credential Creation Modal" to create a + new credential +

+
+
+ + 3 + +
+

Fill out the form with reputation claims:

+
    +
  • + Participation Duration: Select from 3+, 6+, or 12+ months +
  • +
  • Risk Level: Conservative, Moderate, or Aggressive
  • +
  • + Performance Tier: No liquidations, Stable participant, or + Recovered events +
  • +
  • Pool Type Experience: Select one or more pool types
  • +
+
+
+
+ + 4 + +

+ Optionally provide an issuer name and expiration date +

+
+
+ + 5 + +

+ Submit the form to create the credential +

+
+
+ + 6 + +

+ Check the "Stored Credentials" section to see your + created credentials +

+
+
+
+
+
+ + {/* Credential Creation Modal */} + setIsModalOpen(false)} + client={client} + /> +
+ ); +} diff --git a/frontend/src/app/dev/credentials-dashboard/page.tsx b/frontend/src/app/dev/credentials-dashboard/page.tsx new file mode 100644 index 00000000..ebde73b8 --- /dev/null +++ b/frontend/src/app/dev/credentials-dashboard/page.tsx @@ -0,0 +1,22 @@ +"use client"; + +import React from "react"; +import { CredentialsDashboardPage } from "@/components/modules/credentials/ui/pages/CredentialsDashboardPage"; +import { ActaClient } from "@/lib/acta/client"; + +/** + * Test page for the credentials dashboard + */ +export default function CredentialsDashboardTestPage() { + // Initialize Acta client (in a real app, this would come from context/config) + const client = new ActaClient({ + baseUrl: process.env.NEXT_PUBLIC_ACTA_API_URL || "http://localhost:3001", + apiKey: process.env.NEXT_PUBLIC_ACTA_API_KEY, + }); + + return ( +
+ +
+ ); +} diff --git a/frontend/src/app/dev/modals-with-credentials/page.tsx b/frontend/src/app/dev/modals-with-credentials/page.tsx new file mode 100644 index 00000000..03679c01 --- /dev/null +++ b/frontend/src/app/dev/modals-with-credentials/page.tsx @@ -0,0 +1,269 @@ +"use client"; + +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { BorrowModal } from "@/components/modules/marketplace/ui/components/BorrowModal"; +import { SupplyUSDCModal } from "@/components/modules/marketplace/ui/components/SupplyUSDCModal"; + +/** + * Test page demonstrating credential integration in existing modals + */ +export default function ModalsWithCredentialsPage() { + const [isBorrowModalOpen, setIsBorrowModalOpen] = useState(false); + const [isSupplyModalOpen, setIsSupplyModalOpen] = useState(false); + + // Mock pool data for demonstration + const mockPoolData = { + name: "USDC Pool", + totalSupplied: "2,400,000", + totalBorrowed: "1,800,000", + utilizationRate: "75%", + reserves: [ + { + symbol: "USDC", + supplied: "2,400,000", + borrowed: "1,800,000", + supplyAPY: "12.5", + borrowAPY: "15.2", + }, + ], + }; + + return ( +
+
+ {/* Header */} +
+

+ + Credential Integration in Pool Modals +

+

+ Demonstration of credential selection integrated into existing + borrow and supply modals +

+
+ + {/* Integration Overview */} + + + Integration Overview + + Credential selection has been integrated into the existing pool + modals without disrupting the core flows + + + +
+
+

+ + BorrowModal Integration +

+
    +
  • + + Optional credential selection +
  • +
  • + + Better interest rate hints +
  • +
  • + + Non-blocking integration +
  • +
+
+ +
+

+ + SupplyUSDCModal Integration +

+
    +
  • + + Optional credential selection +
  • +
  • + + Higher APY and fee reduction hints +
  • +
  • + + Seamless user experience +
  • +
+
+
+
+
+ + {/* Test Modals */} +
+ {/* Borrow Modal Test */} + + + + + Borrow Modal + + + Test the borrow modal with credential selection + + + +
+
+ + + Pool Information + +
+
+
Pool: {mockPoolData.name}
+
Total Supplied: ${mockPoolData.totalSupplied}
+
Utilization: {mockPoolData.utilizationRate}
+
+
+ + + +
+ + Try selecting a credential to see the benefits hint +
+
+
+ + {/* Supply Modal Test */} + + + + + Supply Modal + + + Test the supply modal with credential selection + + + +
+
+ + + Pool Information + +
+
+
Asset: USDC
+
Current APY: 12.5%
+
Pool Health: Excellent
+
+
+ + + +
+ + Try selecting a credential to see the APY bonus hint +
+
+
+
+ + {/* Implementation Details */} + + + Implementation Details + + How the credential integration works in the existing modals + + + +
+
+
Key Features:
+
    +
  • + • Non-intrusive: Credential selection is + optional and doesn't block core flows +
  • +
  • + • Contextual hints: Different benefits + shown for borrow vs supply +
  • +
  • + • Visual feedback: Green success state when + credential is selected +
  • +
  • + • Seamless integration: Uses existing modal + styling and patterns +
  • +
+
+ +
+
+ Benefits Displayed: +
+
    +
  • + • Borrow Modal: "You may qualify for + better interest rates" +
  • +
  • + • Supply Modal: "You may qualify for + higher APY and reduced fees" +
  • +
  • + • Both: Show selected credential details + (risk level, performance tier) +
  • +
+
+
+
+
+ + {/* Modals */} + setIsBorrowModalOpen(false)} + poolData={mockPoolData} + poolId="test-pool" + /> + + setIsSupplyModalOpen(false)} + onSuccess={() => { + setIsSupplyModalOpen(false); + console.log("Supply transaction successful"); + }} + /> +
+
+ ); +} diff --git a/frontend/src/components/modules/credentials/hooks/useCredentialCreate.ts b/frontend/src/components/modules/credentials/hooks/useCredentialCreate.ts new file mode 100644 index 00000000..795c726f --- /dev/null +++ b/frontend/src/components/modules/credentials/hooks/useCredentialCreate.ts @@ -0,0 +1,244 @@ +/** + * Hook for managing credential creation + * + * Provides functionality to create pool participation credentials using the Acta client, + * with proper error handling, loading states, and local storage management. + */ + +import { useState, useCallback } from "react"; +import { + ActaClient, + CreateCredentialBody, + CredentialData, +} from "@/lib/acta/client"; +import { + PoolParticipationCredentialData, + ReputationClaims, + LocalCredentialRecord, +} from "@/@types/acta.types"; + +// Re-export LocalCredentialRecord for convenience +export type { LocalCredentialRecord } from "@/@types/acta.types"; + +/** + * Credential creation form data + */ +export interface CredentialCreateFormData { + reputationClaims: ReputationClaims; + issuer?: string; + expirationDate?: string; +} + +/** + * Credential creation result (from Acta API) + */ +export interface CredentialCreateResult { + contractId: string; + hash: string; + transactionHash: string; + createdAt: string; + ledgerSequence: number; +} + +/** + * Hook return type + */ +export interface UseCredentialCreateReturn { + // State + isLoading: boolean; + error: string | null; + + // Actions + createCredential: ( + formData: CredentialCreateFormData, + ) => Promise; + clearError: () => void; +} + +/** + * Hook configuration + */ +export interface UseCredentialCreateConfig { + client: ActaClient; + onSuccess?: (result: CredentialCreateResult) => void; + onError?: (error: string) => void; +} + +/** + * Local storage key for credential index + */ +const CREDENTIAL_INDEX_KEY = "tb_vc_index_v1"; + +/** + * Generate a unique local ID + */ +function generateLocalId(): string { + return `local_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +/** + * Save credential record to localStorage + */ +function saveCredentialToLocalStorage(record: LocalCredentialRecord): void { + try { + const existingIndex = localStorage.getItem(CREDENTIAL_INDEX_KEY); + const index: LocalCredentialRecord[] = existingIndex + ? JSON.parse(existingIndex) + : []; + + index.push(record); + localStorage.setItem(CREDENTIAL_INDEX_KEY, JSON.stringify(index)); + } catch (error) { + console.error("Failed to save credential to localStorage:", error); + // Don't throw - this is not critical for credential creation + } +} + +/** + * Hook for credential creation + */ +export function useCredentialCreate( + config: UseCredentialCreateConfig, +): UseCredentialCreateReturn { + const { client, onSuccess, onError } = config; + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const clearError = useCallback(() => { + setError(null); + }, []); + + const createCredential = useCallback( + async ( + formData: CredentialCreateFormData, + ): Promise => { + setIsLoading(true); + setError(null); + + try { + // Prepare credential data + const credentialData: PoolParticipationCredentialData = { + type: "PoolParticipationCredential", + credentialSubject: { + reputationClaims: formData.reputationClaims, + }, + issuer: formData.issuer, + issuanceDate: new Date().toISOString(), + expirationDate: formData.expirationDate, + }; + + // Prepare request body for Acta client + const requestBody: CreateCredentialBody = { + data: credentialData, + metadata: { + createdAt: new Date().toISOString(), + source: "trustbridge-frontend", + }, + }; + + // Create credential via Acta client + const response = await client.createCredential(requestBody); + + if (!response.success) { + throw new Error(response.error || "Failed to create credential"); + } + console.log("response", response); + // The API response has nested data structure: response.data.data + const responseData = response.data as unknown as { + data: CredentialData; + }; + const credentialResponse = responseData.data; + + const result: CredentialCreateResult = { + contractId: credentialResponse.contractId, + hash: credentialResponse.hash, + transactionHash: credentialResponse.transactionHash, + createdAt: credentialResponse.createdAt, + ledgerSequence: credentialResponse.ledgerSequence, + }; + + // Save to localStorage for local indexing (optional convenience feature) + try { + const localId = generateLocalId(); + const localRecord: LocalCredentialRecord = { + localId, + contractId: credentialResponse.contractId, + hash: credentialResponse.hash, + displayData: { + type: credentialData.type, + participationDuration: + formData.reputationClaims.participationDuration, + riskLevel: formData.reputationClaims.riskLevel, + performanceTier: formData.reputationClaims.performanceTier, + poolTypeExperience: formData.reputationClaims.poolTypeExperience, + issuer: formData.issuer, + }, + createdAt: new Date().toISOString(), + }; + saveCredentialToLocalStorage(localRecord); + console.log("Saved to localStorage:", localRecord); + } catch (localStorageError) { + console.warn( + "Failed to save credential to localStorage:", + localStorageError, + ); + } + + // Call success callback + onSuccess?.(result); + + return result; + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Unknown error occurred"; + setError(errorMessage); + onError?.(errorMessage); + return null; + } finally { + setIsLoading(false); + } + }, + [client, onSuccess, onError], + ); + + return { + isLoading, + error, + createCredential, + clearError, + }; +} + +/** + * Utility function to get all credentials from localStorage + */ +export function getCredentialsFromLocalStorage(): LocalCredentialRecord[] { + try { + const data = localStorage.getItem(CREDENTIAL_INDEX_KEY); + return data ? JSON.parse(data) : []; + } catch (error) { + console.error("Failed to read credentials from localStorage:", error); + return []; + } +} + +/** + * Utility function to remove a credential from localStorage + */ +export function removeCredentialFromLocalStorage(localId: string): boolean { + try { + const existingIndex = localStorage.getItem(CREDENTIAL_INDEX_KEY); + const index: LocalCredentialRecord[] = existingIndex + ? JSON.parse(existingIndex) + : []; + + const filteredIndex = index.filter((record) => record.localId !== localId); + localStorage.setItem(CREDENTIAL_INDEX_KEY, JSON.stringify(filteredIndex)); + + return filteredIndex.length < index.length; + } catch (error) { + console.error("Failed to remove credential from localStorage:", error); + return false; + } +} diff --git a/frontend/src/components/modules/credentials/hooks/useCredentials.ts b/frontend/src/components/modules/credentials/hooks/useCredentials.ts new file mode 100644 index 00000000..469c6032 --- /dev/null +++ b/frontend/src/components/modules/credentials/hooks/useCredentials.ts @@ -0,0 +1,341 @@ +/** + * Hook for managing credentials - fetching, filtering, and status updates + * + * Provides functionality to load credentials from localStorage and fetch + * their current status from the Acta API, with optimistic updates for status changes. + */ + +import { useState, useEffect, useCallback } from "react"; +import { ActaClient } from "@/lib/acta/client"; +import { + LocalCredentialRecord, + getCredentialsFromLocalStorage, + removeCredentialFromLocalStorage, +} from "./useCredentialCreate"; + +/** + * Extended credential record with status information + */ +export interface CredentialWithStatus extends LocalCredentialRecord { + status?: "Active" | "Revoked" | "Suspended"; + lastChecked?: string; + isFetching?: boolean; +} + +/** + * Status filter options + */ +export type StatusFilter = "All" | "Active" | "Revoked" | "Suspended"; + +/** + * Hook configuration + */ +export interface UseCredentialsConfig { + client: ActaClient; + autoRefresh?: boolean; + refreshInterval?: number; // in milliseconds +} + +/** + * Hook return type + */ +export interface UseCredentialsReturn { + // State + credentials: CredentialWithStatus[]; + isLoading: boolean; + error: string | null; + + // Filters + statusFilter: StatusFilter; + setStatusFilter: (filter: StatusFilter) => void; + + // Actions + refreshCredentials: () => Promise; + updateCredentialStatus: ( + contractId: string, + status: "Active" | "Revoked" | "Suspended", + ) => Promise; + removeCredential: (localId: string) => boolean; + + // Computed + filteredCredentials: CredentialWithStatus[]; + credentialsByStatus: Record; +} + +/** + * Fetch credential status from Acta API + */ +async function fetchCredentialStatus( + client: ActaClient, + contractId: string, +): Promise<"Active" | "Revoked" | "Suspended"> { + try { + const response = await client.getByContractId(contractId); + + if (!response.success) { + console.warn(`Failed to fetch status for ${contractId}:`, response.error); + return "Active"; // Default to Active if we can't fetch + } + + // Assuming the API returns a status field + const credentialData = response.data as { + status?: string; + credential?: { status?: string }; + }; + const status = + credentialData?.status || credentialData?.credential?.status || "Active"; + + // Normalize status values + switch (status.toLowerCase()) { + case "active": + case "issued": + return "Active"; + case "revoked": + return "Revoked"; + case "suspended": + return "Suspended"; + default: + return "Active"; + } + } catch (error) { + console.warn(`Error fetching status for ${contractId}:`, error); + return "Active"; // Default to Active on error + } +} + +/** + * Update credential status via Acta API + */ +async function updateCredentialStatusOnServer( + client: ActaClient, + contractId: string, + status: "Active" | "Revoked" | "Suspended", +): Promise { + try { + const response = await client.updateStatus(contractId, status); + return response.success; + } catch (updateError) { + console.error(`Error updating status for ${contractId}:`, updateError); + return false; + } +} + +/** + * Hook for managing credentials + */ +export function useCredentials( + config: UseCredentialsConfig, +): UseCredentialsReturn { + const { client, autoRefresh = true, refreshInterval = 30000 } = config; + + const [credentials, setCredentials] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [statusFilter, setStatusFilter] = useState("All"); + + /** + * Load credentials from localStorage and fetch their status + */ + const refreshCredentials = useCallback(async () => { + setIsLoading(true); + setError(null); + + try { + // Load from localStorage + const localCredentials = getCredentialsFromLocalStorage(); + + // Convert to CredentialWithStatus and fetch status for each + const credentialsWithStatus: CredentialWithStatus[] = await Promise.all( + localCredentials.map(async (credential) => { + const credentialWithStatus: CredentialWithStatus = { + ...credential, + isFetching: true, + }; + + try { + const status = await fetchCredentialStatus( + client, + credential.contractId, + ); + return { + ...credentialWithStatus, + status, + lastChecked: new Date().toISOString(), + isFetching: false, + }; + } catch (error) { + console.warn( + `Failed to fetch status for ${credential.contractId}:`, + error, + ); + return { + ...credentialWithStatus, + status: "Active" as const, + lastChecked: new Date().toISOString(), + isFetching: false, + }; + } + }), + ); + + setCredentials(credentialsWithStatus); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : "Failed to load credentials"; + setError(errorMessage); + } finally { + setIsLoading(false); + } + }, [client]); + + /** + * Update credential status with optimistic updates + */ + const updateCredentialStatus = useCallback( + async ( + contractId: string, + newStatus: "Active" | "Revoked" | "Suspended", + ): Promise => { + // Optimistic update + setCredentials((prevCredentials) => + prevCredentials.map((credential) => + credential.contractId === contractId + ? { ...credential, status: newStatus } + : credential, + ), + ); + + try { + // Update on server + const success = await updateCredentialStatusOnServer( + client, + contractId, + newStatus, + ); + + if (!success) { + // Rollback optimistic update on error + setCredentials((prevCredentials) => + prevCredentials.map((credential) => + credential.contractId === contractId + ? { ...credential, status: "Active" } // Rollback to default + : credential, + ), + ); + return false; + } + + return true; + } catch { + // Rollback optimistic update on error + setCredentials((prevCredentials) => + prevCredentials.map((credential) => + credential.contractId === contractId + ? { ...credential, status: "Active" } // Rollback to default + : credential, + ), + ); + return false; + } + }, + [client], + ); + + /** + * Remove credential from localStorage + */ + const removeCredential = useCallback((localId: string): boolean => { + const success = removeCredentialFromLocalStorage(localId); + if (success) { + setCredentials((prevCredentials) => + prevCredentials.filter((credential) => credential.localId !== localId), + ); + } + return success; + }, []); + + // Load credentials on mount + useEffect(() => { + refreshCredentials(); + }, [refreshCredentials]); + + // Auto-refresh if enabled + useEffect(() => { + if (!autoRefresh) return; + + const interval = setInterval(() => { + refreshCredentials(); + }, refreshInterval); + + return () => clearInterval(interval); + }, [autoRefresh, refreshInterval, refreshCredentials]); + + // Computed values + const filteredCredentials = credentials.filter((credential) => { + if (statusFilter === "All") return true; + return credential.status === statusFilter; + }); + + const credentialsByStatus = credentials.reduce( + (acc, credential) => { + const status = credential.status || "Active"; + acc[status] = (acc[status] || 0) + 1; + acc["All"] = (acc["All"] || 0) + 1; + return acc; + }, + {} as Record, + ); + + return { + credentials, + isLoading, + error, + statusFilter, + setStatusFilter, + refreshCredentials, + updateCredentialStatus, + removeCredential, + filteredCredentials, + credentialsByStatus, + }; +} + +/** + * Utility function to format credential summary for sharing + */ +export function formatCredentialSummary( + credential: CredentialWithStatus, + showContractDetails: boolean = false, +): string { + const { displayData, contractId, hash } = credential; + + let summary = `TrustBridge — Pool Participation Credential\n`; + summary += `• Duration: ${displayData.participationDuration}\n`; + summary += `• Risk: ${displayData.riskLevel}\n`; + summary += `• Performance: ${displayData.performanceTier}\n`; + summary += `• Pools: ${displayData.poolTypeExperience.join(", ")}\n`; + + if (displayData.issuer) { + summary += `• Issuer: ${displayData.issuer}\n`; + } + + if (showContractDetails && contractId && hash) { + summary += `\nContract ID: ${contractId.substring(0, 8)}...\n`; + summary += `Hash: ${hash.substring(0, 8)}...`; + } + + return summary; +} + +/** + * Utility function to copy text to clipboard + */ +export async function copyToClipboard(text: string): Promise { + try { + await navigator.clipboard.writeText(text); + return true; + } catch (clipboardError) { + console.error("Failed to copy to clipboard:", clipboardError); + return false; + } +} diff --git a/frontend/src/components/modules/credentials/ui/components/CreateCredentialModal.tsx b/frontend/src/components/modules/credentials/ui/components/CreateCredentialModal.tsx new file mode 100644 index 00000000..33ad6894 --- /dev/null +++ b/frontend/src/components/modules/credentials/ui/components/CreateCredentialModal.tsx @@ -0,0 +1,525 @@ +"use client"; + +import React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { toast } from "sonner"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + useCredentialCreate, + type CredentialCreateFormData, +} from "@/components/modules/credentials/hooks/useCredentialCreate"; +import { ActaClient } from "@/lib/acta/client"; +import { + ParticipationDuration, + CredentialRiskLevel, + PerformanceTier, + PoolTypeExperience, +} from "@/@types/acta.types"; + +/** + * Form validation schema + */ +const credentialFormSchema = z.object({ + reputationClaims: z.object({ + participationDuration: z.enum( + ["3+ months", "6+ months", "12+ months"] as const, + { + required_error: "Please select a participation duration.", + }, + ), + riskLevel: z.enum(["Conservative", "Moderate", "Aggressive"] as const, { + required_error: "Please select a risk level.", + }), + performanceTier: z.enum( + ["No liquidations", "Stable participant", "Recovered events"] as const, + { + required_error: "Please select a performance tier.", + }, + ), + poolTypeExperience: z + .array(z.enum(["Multi-asset", "Stablecoin", "LSD", "LP-Perp"] as const)) + .min(1, "Please select at least one pool type experience."), + }), + issuer: z.string().optional(), + expirationDate: z.string().optional(), +}); + +type CredentialFormValues = z.infer; + +/** + * Modal props + */ +interface CreateCredentialModalProps { + isOpen: boolean; + onClose: () => void; + client: ActaClient; + onSuccess?: () => void; +} + +/** + * Available options for form fields + */ +const PARTICIPATION_DURATION_OPTIONS: { + value: ParticipationDuration; + label: string; +}[] = [ + { value: "3+ months", label: "3+ months" }, + { value: "6+ months", label: "6+ months" }, + { value: "12+ months", label: "12+ months" }, +]; + +const RISK_LEVEL_OPTIONS: { + value: CredentialRiskLevel; + label: string; + description: string; +}[] = [ + { + value: "Conservative", + label: "Conservative", + description: "Low risk, stable returns", + }, + { + value: "Moderate", + label: "Moderate", + description: "Balanced risk and reward", + }, + { + value: "Aggressive", + label: "Aggressive", + description: "High risk, high potential returns", + }, +]; + +const PERFORMANCE_TIER_OPTIONS: { + value: PerformanceTier; + label: string; + description: string; +}[] = [ + { + value: "No liquidations", + label: "No liquidations", + description: "Clean track record", + }, + { + value: "Stable participant", + label: "Stable participant", + description: "Consistent performance", + }, + { + value: "Recovered events", + label: "Recovered events", + description: "Bounced back from issues", + }, +]; + +const POOL_TYPE_OPTIONS: { + value: PoolTypeExperience; + label: string; + description: string; +}[] = [ + { + value: "Multi-asset", + label: "Multi-asset", + description: "Diversified portfolio pools", + }, + { + value: "Stablecoin", + label: "Stablecoin", + description: "Stable value pools", + }, + { value: "LSD", label: "LSD", description: "Liquid staking derivatives" }, + { + value: "LP-Perp", + label: "LP-Perp", + description: "Liquidity provision perpetuals", + }, +]; + +/** + * Create Credential Modal Component + */ +export function CreateCredentialModal({ + isOpen, + onClose, + client, + onSuccess, +}: CreateCredentialModalProps) { + const form = useForm({ + resolver: zodResolver(credentialFormSchema), + defaultValues: { + reputationClaims: { + participationDuration: undefined, + riskLevel: undefined, + performanceTier: undefined, + poolTypeExperience: [], + }, + issuer: "", + expirationDate: "", + }, + }); + + const { createCredential, isLoading, error, clearError } = + useCredentialCreate({ + client, + onSuccess: (result) => { + const contractId = result.contractId || "Unknown"; + const contractDisplay = + contractId.length > 8 + ? contractId.substring(0, 8) + "..." + : contractId; + + toast.success("Credential Created Successfully", { + description: `Stored on Stellar blockchain. Contract: ${contractDisplay}`, + }); + form.reset(); + onSuccess?.(); + onClose(); + }, + onError: (errorMessage) => { + toast.error("Failed to Create Credential", { + description: errorMessage, + }); + }, + }); + + const onSubmit = async (values: CredentialFormValues) => { + clearError(); + + const formData: CredentialCreateFormData = { + reputationClaims: values.reputationClaims, + issuer: values.issuer || undefined, + expirationDate: values.expirationDate || undefined, + }; + + await createCredential(formData); + }; + + const handleClose = () => { + if (!isLoading) { + form.reset(); + clearError(); + onClose(); + } + }; + + return ( + + + +
+ + + Create Pool Participation Credential + +
+ + Create a privacy-preserving credential that demonstrates your pool + participation experience. This information will be used to establish + your reputation in the ecosystem. + +
+ +
+ + {/* Participation Duration */} + ( + + + + Participation Duration + + + + + )} + /> + + {/* Risk Level */} + ( + + + + Risk Level + + + + + )} + /> + + {/* Performance Tier */} + ( + + + + Performance Tier + + + + + )} + /> + + {/* Pool Type Experience */} + ( + +
+ + + Pool Type Experience + + + Select all pool types you have experience with. + +
+
+ {POOL_TYPE_OPTIONS.map((option) => ( + { + return ( + + + { + return checked + ? field.onChange([ + ...field.value, + option.value, + ]) + : field.onChange( + field.value?.filter( + (value) => value !== option.value, + ), + ); + }} + className="data-[state=checked]:bg-success data-[state=checked]:border-success" + /> + +
+ + {option.label} + + + {option.description} + +
+
+ ); + }} + /> + ))} +
+ +
+ )} + /> + + {/* Optional Fields */} +
+ {/* Issuer */} + ( + + + + Issuer (Optional) + + + + + + Organization or entity issuing this credential + + + + )} + /> + + {/* Expiration Date */} + ( + + + + Expiration Date (Optional) + + + + + + When this credential expires (leave blank for no + expiration) + + + + )} + /> +
+ + {/* Error Display */} + {error && ( +
+
+ +

Error

+
+

{error}

+
+ )} + + + + + + + +
+
+ ); +} diff --git a/frontend/src/components/modules/credentials/ui/components/CredentialCard.tsx b/frontend/src/components/modules/credentials/ui/components/CredentialCard.tsx new file mode 100644 index 00000000..a1d459dd --- /dev/null +++ b/frontend/src/components/modules/credentials/ui/components/CredentialCard.tsx @@ -0,0 +1,378 @@ +"use client"; + +import React, { useState } from "react"; +import { toast } from "sonner"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { CredentialWithStatus } from "@/components/modules/credentials/hooks/useCredentials"; +import { + formatCredentialSummary, + copyToClipboard, +} from "@/components/modules/credentials/hooks/useCredentials"; + +/** + * Props for CredentialCard component + */ +interface CredentialCardProps { + credential: CredentialWithStatus; + onStatusUpdate: ( + contractId: string, + status: "Active" | "Revoked" | "Suspended", + ) => Promise; + onRemove?: (localId: string) => void; + showActions?: boolean; +} + +/** + * Status color mapping + */ +const statusColors = { + Active: "bg-green-900/20 text-green-300 border-green-700", + Revoked: "bg-red-900/20 text-red-300 border-red-700", + Suspended: "bg-yellow-900/20 text-yellow-300 border-yellow-700", +} as const; + +/** + * Status icons + */ +const statusIcons = { + Active: "fas fa-check-circle", + Revoked: "fas fa-times-circle", + Suspended: "fas fa-pause-circle", +} as const; + +/** + * CredentialCard Component + */ +export function CredentialCard({ + credential, + onStatusUpdate, + onRemove, + showActions = true, +}: CredentialCardProps) { + const [isUpdating, setIsUpdating] = useState(false); + const [showContractDetails, setShowContractDetails] = useState(false); + const [isSharing, setIsSharing] = useState(false); + + const { displayData, contractId, hash, status, createdAt, isFetching } = + credential; + + const handleStatusUpdate = async ( + newStatus: "Active" | "Revoked" | "Suspended", + ) => { + if (isUpdating) return; + + setIsUpdating(true); + try { + const success = await onStatusUpdate(contractId, newStatus); + if (success) { + toast.success(`Credential status updated to ${newStatus}`); + } else { + toast.error(`Failed to update credential status`); + } + } catch { + toast.error(`Error updating credential status`); + } finally { + setIsUpdating(false); + } + }; + + const handleShare = async () => { + setIsSharing(true); + try { + const summary = formatCredentialSummary(credential, showContractDetails); + const success = await copyToClipboard(summary); + + if (success) { + toast.success("Credential summary copied to clipboard"); + } else { + toast.error("Failed to copy to clipboard"); + } + } catch { + toast.error("Error sharing credential"); + } finally { + setIsSharing(false); + } + }; + + const handleRemove = () => { + if ( + onRemove && + window.confirm( + "Are you sure you want to remove this credential from your local index?", + ) + ) { + onRemove(credential.localId); + toast.success("Credential removed from local index"); + } + }; + + const currentStatus = status || "Active"; + const statusColor = statusColors[currentStatus]; + const statusIcon = statusIcons[currentStatus]; + + return ( + + +
+
+ +
+

{displayData.type}

+

+ Created {new Date(createdAt).toLocaleDateString()} +

+
+
+ +
+ + + {currentStatus} + {isFetching && } + + + {showActions && ( + + + + + + + + {isSharing ? "Copying..." : "Copy Summary"} + + + + + handleStatusUpdate("Active")} + disabled={isUpdating || currentStatus === "Active"} + > + + Set Active + + + handleStatusUpdate("Suspended")} + disabled={isUpdating || currentStatus === "Suspended"} + > + + Suspend + + + handleStatusUpdate("Revoked")} + disabled={isUpdating || currentStatus === "Revoked"} + > + + Revoke + + + {onRemove && ( + <> + + + + Remove from Local Index + + + )} + + + )} +
+
+
+ + + {/* Credential Details */} +
+
+ +
+

Duration

+

+ {displayData.participationDuration} +

+
+
+ +
+ +
+

Risk Level

+

+ {displayData.riskLevel} +

+
+
+ +
+ +
+

Performance

+

+ {displayData.performanceTier} +

+
+
+ +
+ +
+

Pool Types

+

+ {displayData.poolTypeExperience.join(", ")} +

+
+
+
+ + {/* Issuer */} + {displayData.issuer && ( +
+ +
+

Issuer

+

+ {displayData.issuer} +

+
+
+ )} + + {/* Contract Details (Collapsible) */} + + + + + + + + Contract Information + + + Blockchain details for this credential + + + +
+
+ +

+ {contractId} +

+
+ +
+ +

{hash}

+
+ +
+ + + + {currentStatus} + +
+
+
+
+ + {/* Share Dialog */} + + + + + + + Share Credential + + Copy a summary of this credential to share + + + +
+
+ + +
+ +
+
+                  {formatCredentialSummary(credential, showContractDetails)}
+                
+
+ + +
+
+
+
+
+ ); +} diff --git a/frontend/src/components/modules/credentials/ui/components/CredentialSelector.tsx b/frontend/src/components/modules/credentials/ui/components/CredentialSelector.tsx new file mode 100644 index 00000000..885b814e --- /dev/null +++ b/frontend/src/components/modules/credentials/ui/components/CredentialSelector.tsx @@ -0,0 +1,287 @@ +"use client"; + +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { CredentialWithStatus } from "@/components/modules/credentials/hooks/useCredentials"; +import { ActaClient } from "@/lib/acta/client"; +import { useCredentials } from "@/components/modules/credentials/hooks/useCredentials"; + +/** + * Props for CredentialSelector component + */ +interface CredentialSelectorProps { + client: ActaClient; + onCredentialSelect: (credential: CredentialWithStatus) => void; + selectedCredential?: CredentialWithStatus; + disabled?: boolean; + placeholder?: string; +} + +/** + * Credential option component for selection + */ +function CredentialOption({ + credential, + isSelected, + onSelect, +}: { + credential: CredentialWithStatus; + isSelected: boolean; + onSelect: () => void; +}) { + const { displayData, status, contractId } = credential; + const currentStatus = status || "Active"; + + const statusColors = { + Active: "bg-green-900/20 text-green-300 border-green-700", + Revoked: "bg-red-900/20 text-red-300 border-red-700", + Suspended: "bg-yellow-900/20 text-yellow-300 border-yellow-700", + }; + + return ( +
+
+
+
+ + + Pool Participation Credential + +
+ + {currentStatus} + +
+ +
+
+ Duration: + + {displayData.participationDuration} + +
+
+ Risk: + {displayData.riskLevel} +
+
+ Performance: + + {displayData.performanceTier} + +
+
+ Pools: + + {displayData.poolTypeExperience.join(", ")} + +
+
+ +
+ Contract: {contractId.substring(0, 8)}... +
+
+
+ ); +} + +/** + * Main CredentialSelector component + */ +export function CredentialSelector({ + client, + onCredentialSelect, + selectedCredential, + disabled = false, + placeholder = "Select a credential to verify your reputation", +}: CredentialSelectorProps) { + const [isOpen, setIsOpen] = useState(false); + + const { credentials, isLoading, error, refreshCredentials } = useCredentials({ + client, + autoRefresh: false, + }); + + // Filter for active credentials only + const activeCredentials = credentials.filter( + (c) => (c.status || "Active") === "Active", + ); + + const handleCredentialSelect = (credential: CredentialWithStatus) => { + onCredentialSelect(credential); + setIsOpen(false); + }; + + const handleRefresh = async () => { + await refreshCredentials(); + }; + + return ( + + + + + + + + + + Select Credential + + + Choose a credential to verify your pool participation experience and + reputation + + + +
+ {/* Refresh Button */} +
+ +
+ + {/* Error State */} + {error && ( +
+
+ +

Failed to load credentials

+

{error}

+
+
+ )} + + {/* Loading State */} + {isLoading && ( +
+ {[1, 2].map((i) => ( +
+
+
+
+
+
+
+
+
+
+
+ {[1, 2, 3, 4].map((j) => ( +
+
+
+
+ ))} +
+
+
+
+ ))} +
+ )} + + {/* Empty State */} + {!isLoading && !error && activeCredentials.length === 0 && ( +
+
+ +

+ No Active Credentials +

+

+ You don't have any active credentials yet. Create one to + verify your reputation. +

+ +
+
+ )} + + {/* Credentials List */} + {!isLoading && !error && activeCredentials.length > 0 && ( +
+ {activeCredentials.map((credential) => ( + handleCredentialSelect(credential)} + /> + ))} +
+ )} + + {/* Info */} + {activeCredentials.length > 0 && ( +
+
+ +
+

+ Why select a credential? +

+

+ Credentials help verify your experience and may unlock + better terms or features in pools. +

+
+
+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/modules/credentials/ui/pages/CredentialsDashboardPage.tsx b/frontend/src/components/modules/credentials/ui/pages/CredentialsDashboardPage.tsx new file mode 100644 index 00000000..cbc97226 --- /dev/null +++ b/frontend/src/components/modules/credentials/ui/pages/CredentialsDashboardPage.tsx @@ -0,0 +1,398 @@ +"use client"; + +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { CreateCredentialModal } from "@/components/modules/credentials/ui/components/CreateCredentialModal"; +import { CredentialCard } from "@/components/modules/credentials/ui/components/CredentialCard"; +import { + useCredentials, + type StatusFilter, +} from "@/components/modules/credentials/hooks/useCredentials"; +import { ActaClient } from "@/lib/acta/client"; +import { toast } from "sonner"; + +/** + * Props for CredentialsDashboardPage component + */ +interface CredentialsDashboardPageProps { + client: ActaClient; +} + +/** + * Empty state component + */ +function EmptyState({ + onCreateCredential, +}: { + onCreateCredential: () => void; +}) { + return ( + + +
+ +

+ No Credentials Yet +

+

+ Create your first pool participation credential to establish your + reputation in the ecosystem and unlock enhanced features. +

+
+ + +
+
+ ); +} + +/** + * No results for filter component + */ +function NoResultsForFilter({ + statusFilter, + onCreateCredential, + onClearFilter, +}: { + statusFilter: StatusFilter; + onCreateCredential: () => void; + onClearFilter: () => void; +}) { + const statusIcons = { + Active: "fas fa-check-circle", + Revoked: "fas fa-times-circle", + Suspended: "fas fa-pause-circle", + } as const; + + const statusColors = { + Active: "text-green-400", + Revoked: "text-red-400", + Suspended: "text-yellow-400", + } as const; + + return ( + + +
+ +

+ No {statusFilter} Credentials +

+

+ {statusFilter === "All" + ? "You don't have any credentials yet." + : `You don't have any credentials with status "${statusFilter}".`} +

+
+ +
+ {statusFilter !== "All" ? ( + + ) : ( + + )} +
+
+
+ ); +} + +/** + * Loading state component + */ +function LoadingState() { + return ( +
+ {[1, 2, 3].map((i) => ( + + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+ {[1, 2, 3, 4].map((j) => ( +
+
+
+
+ ))} +
+
+
+
+
+ ))} +
+ ); +} + +/** + * Stats component + */ +function StatsSection({ + credentialsByStatus, +}: { + credentialsByStatus: Record; +}) { + const stats = [ + { + label: "Total Credentials", + value: credentialsByStatus.All || 0, + icon: "fas fa-certificate", + color: "text-success", + }, + { + label: "Active", + value: credentialsByStatus.Active || 0, + icon: "fas fa-check-circle", + color: "text-green-400", + }, + { + label: "Suspended", + value: credentialsByStatus.Suspended || 0, + icon: "fas fa-pause-circle", + color: "text-yellow-400", + }, + { + label: "Revoked", + value: credentialsByStatus.Revoked || 0, + icon: "fas fa-times-circle", + color: "text-red-400", + }, + ]; + + return ( +
+ {stats.map((stat) => ( + + +
+ +
+

{stat.value}

+

{stat.label}

+
+
+
+
+ ))} +
+ ); +} + +/** + * Main CredentialsDashboardPage component + */ +export function CredentialsDashboardPage({ + client, +}: CredentialsDashboardPageProps) { + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + + const { + credentials, + filteredCredentials, + isLoading, + error, + statusFilter, + setStatusFilter, + refreshCredentials, + updateCredentialStatus, + removeCredential, + credentialsByStatus, + } = useCredentials({ client }); + + const handleCreateCredential = () => { + setIsCreateModalOpen(true); + }; + + const handleRefresh = async () => { + await refreshCredentials(); + toast.success("Credentials refreshed"); + }; + + if (error) { + return ( +
+ + + +

+ Error Loading Credentials +

+

{error}

+ +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+

+ + Credentials Dashboard +

+

+ Manage your pool participation credentials and reputation +

+
+ +
+ + +
+
+ + {/* Stats */} + +
+ + {/* Filters - Always visible */} +
+ + +
+ + +
+
+
+
+ + {/* Content */} + {isLoading ? ( + + ) : filteredCredentials.length === 0 ? ( + credentials.length === 0 ? ( + + ) : ( + setStatusFilter("All")} + /> + ) + ) : ( +
+ {filteredCredentials.map((credential) => ( + + ))} +
+ )} + + {/* Create Credential Modal */} + setIsCreateModalOpen(false)} + client={client} + onSuccess={() => { + setIsCreateModalOpen(false); + refreshCredentials(); + toast.success("Credential created successfully!"); + }} + /> +
+ ); +} + +// Add missing import for Label +import { Label } from "@/components/ui/label"; diff --git a/frontend/src/components/modules/marketplace/ui/components/BorrowModal.tsx b/frontend/src/components/modules/marketplace/ui/components/BorrowModal.tsx index f13a0bc9..28e01ca0 100644 --- a/frontend/src/components/modules/marketplace/ui/components/BorrowModal.tsx +++ b/frontend/src/components/modules/marketplace/ui/components/BorrowModal.tsx @@ -2,14 +2,17 @@ import { useEffect, useState } from "react"; import { useBorrow } from "../../hooks/useBorrow.hook"; -import { - monitorHealthFactor, - getHealthFactorAlerts, +import { + monitorHealthFactor, + getHealthFactorAlerts, calculateMaxBorrowable, calculateLiquidationPrice, - type HealthFactorResult + type HealthFactorResult, } from "@/helpers/health-factor.helper"; import { useWalletContext } from "@/providers/wallet.provider"; +import { CredentialSelector } from "@/components/modules/credentials/ui/components/CredentialSelector"; +import { CredentialWithStatus } from "@/components/modules/credentials/hooks/useCredentials"; +import { ActaClient } from "@/lib/acta/client"; interface PoolReserve { symbol: string; @@ -36,10 +39,21 @@ interface BorrowModalProps { export function BorrowModal({ isOpen, onClose, poolId }: BorrowModalProps) { const { walletAddress } = useWalletContext(); - const [healthFactor, setHealthFactor] = useState(null); + const [healthFactor, setHealthFactor] = useState( + null, + ); const [alerts, setAlerts] = useState([]); const [maxBorrowable, setMaxBorrowable] = useState(0); const [liquidationPrice, setLiquidationPrice] = useState(0); + const [selectedCredential, setSelectedCredential] = useState< + CredentialWithStatus | undefined + >(); + + // Initialize Acta client (in a real app, this would come from context/config) + const actaClient = new ActaClient({ + baseUrl: process.env.NEXT_PUBLIC_ACTA_API_URL || "http://localhost:3001", + apiKey: process.env.NEXT_PUBLIC_ACTA_API_KEY, + }); const { borrowAmount, @@ -59,20 +73,20 @@ export function BorrowModal({ isOpen, onClose, poolId }: BorrowModalProps) { const stopMonitoring = monitorHealthFactor(walletAddress, (result) => { setHealthFactor(result); setAlerts(getHealthFactorAlerts(result)); - + // Calculate max borrowable amount const maxBorrow = calculateMaxBorrowable( result.collateralValue, 85, // USDC collateral factor - result.borrowedValue + result.borrowedValue, ); setMaxBorrowable(maxBorrow); - + // Calculate liquidation price const liqPrice = calculateLiquidationPrice( result.borrowedValue, result.collateralValue, - 85 // USDC collateral factor + 85, // USDC collateral factor ); setLiquidationPrice(liqPrice); }); @@ -107,17 +121,21 @@ export function BorrowModal({ isOpen, onClose, poolId }: BorrowModalProps) {
- +
{alert}
@@ -158,9 +176,14 @@ export function BorrowModal({ isOpen, onClose, poolId }: BorrowModalProps) { + {/* TODO: Modify handleBorrow to include selectedCredential data + - Pass credential contractId and hash to smart contract + - Include risk level and performance tier for rate calculation + - Verify credential on-chain before applying benefits */} + {/* TODO: Modify handleSupplyUSDC to include selectedCredential data + - Pass credential contractId and hash to smart contract + - Include risk level and performance tier for APY calculation + - Verify credential on-chain before applying benefits */}