Skip to content

Commit 9961728

Browse files
authored
Merge pull request #86 from Trustless-Work/refactor/some-issues
Refactor/some issues
2 parents 6d22744 + ab6509d commit 9961728

File tree

41 files changed

+614
-1114
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+614
-1114
lines changed

apps/backoffice-tokenization/src/components/shared/campaign-card.tsx

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Badge } from "@tokenization/ui/badge";
66
import { Button } from "@tokenization/ui/button";
77
import { CampaignCard as SharedCampaignCard } from "@tokenization/ui/campaign-card";
88
import { cn } from "@tokenization/shared/lib/utils";
9-
import { Landmark } from "lucide-react";
9+
import { Banknote, CheckCircle, Circle, Landmark } from "lucide-react";
1010
import { useGetEscrowFromIndexerByContractIds } from "@trustless-work/escrow";
1111
import type { MultiReleaseMilestone } from "@trustless-work/escrow/types";
1212
import type { Campaign } from "@/features/campaigns/types/campaign.types";
@@ -39,9 +39,12 @@ export function CampaignCard({ campaign }: CampaignCardProps) {
3939
staleTime: 1000 * 60 * 5,
4040
});
4141

42-
const milestones = (escrowData?.milestones ?? []) as MultiReleaseMilestone[];
43-
const assigned = milestones.reduce((sum, m) => sum + fromStroops(m.amount ?? 0), 0);
44-
const progressValue = campaign.poolSize > 0 ? Math.min(100, (assigned / campaign.poolSize) * 100) : 0;
42+
const allMilestones = (escrowData?.milestones ?? []) as MultiReleaseMilestone[];
43+
const visibleMilestones = allMilestones.slice(1);
44+
const assigned = allMilestones.reduce((sum, m) => sum + fromStroops(m.amount ?? 0), 0);
45+
const loansCompleted = visibleMilestones.filter((m) => m.status === "Approved").length;
46+
const totalLoans = visibleMilestones.length;
47+
const progressValue = totalLoans > 0 ? Math.min(100, (loansCompleted / totalLoans) * 100) : 0;
4548

4649
return (
4750
<SharedCampaignCard
@@ -68,16 +71,36 @@ export function CampaignCard({ campaign }: CampaignCardProps) {
6871
footer={
6972
<div className="flex flex-col gap-1">
7073
<span className="text-xs font-bold text-foreground">
71-
USDC {formatCurrency(assigned)} / USDC {formatCurrency(campaign.poolSize)}
74+
<span className="font-bold">Pool Size:</span> USDC {formatCurrency(assigned)} / USDC {formatCurrency(campaign.poolSize)}
7275
</span>
73-
{campaign.vaultId ? (
74-
<span className="text-[10px] text-muted-foreground font-mono truncate max-w-[280px]" title={campaign.vaultId}>
75-
Vault: {campaign.vaultId}
76-
</span>
77-
) : null}
7876
</div>
7977
}
80-
progress={{ label: "Dinero recaudado", value: progressValue }}
81-
/>
78+
progress={{ label: "Loans Completed", value: progressValue }}
79+
>
80+
{visibleMilestones.length > 0 ? (
81+
<>
82+
<p className="text-xs font-semibold uppercase tracking-widest text-text-muted">
83+
Loans
84+
</p>
85+
<ul className="flex flex-col gap-1">
86+
{visibleMilestones.map((m, i) => (
87+
<li key={i} className="flex items-center gap-2 text-xs text-muted-foreground">
88+
{m.flags?.approved ? (
89+
<CheckCircle className="size-3.5 text-green-500 shrink-0" />
90+
) : m.flags?.released ? (
91+
<Banknote className="size-3.5 text-blue-500 shrink-0" />
92+
) : (
93+
<Circle className="size-3.5 shrink-0" />
94+
)}
95+
<span className="truncate">{m.description || `Loan ${i + 1}`}</span>
96+
<span className="ml-auto font-medium">{m.amount} USDC</span>
97+
</li>
98+
))}
99+
</ul>
100+
</>
101+
) : (
102+
<p className="text-xs text-muted-foreground">No loans available.</p>
103+
)}
104+
</SharedCampaignCard>
82105
);
83106
}

apps/backoffice-tokenization/src/features/campaigns/components/loans/manage-loans-view.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,18 +245,17 @@ export function ManageLoansView({ contractId }: ManageLoansViewProps) {
245245
milestones.map((milestone, index) => {
246246
const isApproved = milestone.flags?.approved === true;
247247
const isReleased = milestone.flags?.released === true;
248-
const milestoneAmount = fromStroops(milestone.amount || 0);
248+
const milestoneAmount = milestone.amount
249249
const insufficientFunds = escrowBalance < milestoneAmount;
250250

251251

252252
return (
253253
<div
254254
key={index}
255-
className={`flex items-center justify-between rounded-xl border px-4 py-3 transition-colors ${
256-
isReleased
255+
className={`flex items-center justify-between rounded-xl border px-4 py-3 transition-colors ${isReleased
257256
? "border-border bg-secondary/20 opacity-60"
258257
: "border-border bg-card"
259-
}`}
258+
}`}
260259
>
261260
<div className="flex flex-col gap-0.5">
262261
<span

apps/backoffice-tokenization/src/features/campaigns/hooks/use-create-campaign.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
55
import { zodResolver } from "@hookform/resolvers/zod";
66
import { z } from "zod";
77
import { useRouter } from "next/navigation";
8+
import { useQueryClient } from "@tanstack/react-query";
89
import { useWalletContext } from "@tokenization/tw-blocks-shared/src/wallet-kit/WalletProvider";
910
import { useEscrowsMutations } from "@tokenization/tw-blocks-shared/src/tanstack/useEscrowsMutations";
1011
import {
@@ -112,6 +113,7 @@ const TOTAL_STEPS = 3;
112113

113114
export function useCreateCampaign() {
114115
const router = useRouter();
116+
const queryClient = useQueryClient();
115117
const { walletAddress } = useWalletContext();
116118
const { deployEscrow } = useEscrowsMutations();
117119
const [step, setStep] = useState(1);
@@ -288,6 +290,8 @@ export function useCreateCampaign() {
288290
saveFlowState({ campaignDbId: created.id });
289291
setPhaseStatus(1, "success");
290292

293+
await queryClient.invalidateQueries({ queryKey: ["campaigns"] });
294+
291295
setTimeout(() => {
292296
clearFlowState();
293297
router.push("/campaigns");
@@ -298,7 +302,7 @@ export function useCreateCampaign() {
298302
setPhaseStatus(currentPhase, "error", message);
299303
setDeployFailedAt(currentPhase);
300304
}
301-
}, [walletAddress, router]);
305+
}, [walletAddress, router, queryClient]);
302306

303307
const retryDeploy = useCallback(() => {
304308
if (deployFailedAt === null) return;

apps/backoffice-tokenization/src/features/campaigns/hooks/useToggleVault.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
"use client";
22

33
import { useState } from "react";
4+
import { useQueryClient } from "@tanstack/react-query";
45
import { useWalletContext } from "@tokenization/tw-blocks-shared/src/wallet-kit/WalletProvider";
56
import { signTransaction } from "@tokenization/tw-blocks-shared/src/wallet-kit/wallet-kit";
67
import { submitAndExtractAddress } from "@/features/campaigns/services/soroban.service";
7-
import { enableVault } from "@/features/campaigns/services/campaigns.api";
8+
import {
9+
enableVault,
10+
updateCampaignStatusByVaultId,
11+
} from "@/features/campaigns/services/campaigns.api";
812

913
interface UseToggleVaultParams {
1014
onSuccess?: () => void;
1115
}
1216

1317
export function useToggleVault({ onSuccess }: UseToggleVaultParams = {}) {
1418
const { walletAddress } = useWalletContext();
19+
const queryClient = useQueryClient();
1520
const [isSubmitting, setIsSubmitting] = useState(false);
1621
const [error, setError] = useState<string | null>(null);
1722

@@ -39,6 +44,16 @@ export function useToggleVault({ onSuccess }: UseToggleVaultParams = {}) {
3944

4045
await submitAndExtractAddress(signedXdr);
4146

47+
try {
48+
await updateCampaignStatusByVaultId(
49+
vaultContractId,
50+
enabled ? "CLAIMABLE" : "FUNDRAISING",
51+
);
52+
await queryClient.invalidateQueries({ queryKey: ["campaigns"] });
53+
} catch {
54+
// Campaign may not exist or vaultId not linked; status update is best-effort
55+
}
56+
4257
onSuccess?.();
4358
} catch (e) {
4459
const message = e instanceof Error ? e.message : "Unexpected error";
Lines changed: 40 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,14 @@
1+
import { httpClient } from "@/lib/httpClient";
12
import type { Campaign } from "@/features/campaigns/types/campaign.types";
23

3-
const CORE_API = "/core-api";
4-
5-
const API_KEY = process.env.NEXT_PUBLIC_CORE_API_KEY ?? "";
6-
7-
async function post<T>(path: string, body: unknown): Promise<T> {
8-
const res = await fetch(`${CORE_API}${path}`, {
9-
method: "POST",
10-
headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
11-
body: JSON.stringify(body),
12-
});
13-
if (!res.ok) {
14-
const err = await res.json().catch(() => ({}));
15-
throw new Error(
16-
(err as { message?: string }).message ??
17-
`Error ${res.status} en ${path}`,
18-
);
19-
}
20-
return res.json() as Promise<T>;
21-
}
22-
234
export async function getCampaigns(): Promise<Campaign[]> {
24-
const res = await fetch(`${CORE_API}/campaigns`, {
25-
headers: { "x-api-key": API_KEY },
26-
});
27-
if (!res.ok) throw new Error("No se pudieron cargar las campañas.");
28-
return res.json();
5+
const { data } = await httpClient.get<Campaign[]>("/campaigns");
6+
return data;
297
}
308

319
export async function getCampaignById(id: string): Promise<Campaign> {
32-
const res = await fetch(`${CORE_API}/campaigns/${id}`, {
33-
headers: { "x-api-key": API_KEY },
34-
});
35-
if (!res.ok) throw new Error("No se pudo cargar la campaña.");
36-
return res.json();
10+
const { data } = await httpClient.get<Campaign>(`/campaigns/${id}`);
11+
return data;
3712
}
3813

3914
export async function deployAll(params: {
@@ -46,25 +21,32 @@ export async function deployAll(params: {
4621
maxPerInvestor: number;
4722
callerPublicKey: string;
4823
}): Promise<{ unsignedXdr: string }> {
49-
return post("/deploy/all", params);
24+
const { data } = await httpClient.post<{ unsignedXdr: string }>(
25+
"/deploy/all",
26+
params,
27+
);
28+
return data;
5029
}
5130

5231
export async function updateCampaignStatus(
5332
id: string,
5433
status: string,
5534
): Promise<unknown> {
56-
const res = await fetch(`${CORE_API}/campaigns/${id}/status`, {
57-
method: "PATCH",
58-
headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
59-
body: JSON.stringify({ status }),
35+
const { data } = await httpClient.patch(`/campaigns/${id}/status`, {
36+
status,
6037
});
61-
if (!res.ok) {
62-
const err = await res.json().catch(() => ({}));
63-
throw new Error(
64-
(err as { message?: string }).message ?? `Error ${res.status}`,
65-
);
66-
}
67-
return res.json();
38+
return data;
39+
}
40+
41+
export async function updateCampaignStatusByVaultId(
42+
vaultId: string,
43+
status: string,
44+
): Promise<unknown> {
45+
const { data } = await httpClient.patch(
46+
`/campaigns/by-vault/${vaultId}/status`,
47+
{ status },
48+
);
49+
return data;
6850
}
6951

7052
export async function createCampaign(params: {
@@ -80,35 +62,11 @@ export async function createCampaign(params: {
8062
tokenSaleId: string;
8163
vaultId?: string;
8264
}): Promise<{ id: string }> {
83-
return post("/campaigns", params);
84-
}
85-
86-
async function get<T>(path: string): Promise<T> {
87-
const res = await fetch(`${CORE_API}${path}`, {
88-
headers: { "x-api-key": API_KEY },
89-
});
90-
if (!res.ok) {
91-
const err = await res.json().catch(() => ({}));
92-
throw new Error(
93-
(err as { message?: string }).message ?? `Error ${res.status} on ${path}`,
94-
);
95-
}
96-
return res.json() as Promise<T>;
97-
}
98-
99-
async function patch<T>(path: string, body: unknown): Promise<T> {
100-
const res = await fetch(`${CORE_API}${path}`, {
101-
method: "PATCH",
102-
headers: { "Content-Type": "application/json", "x-api-key": API_KEY },
103-
body: JSON.stringify(body),
104-
});
105-
if (!res.ok) {
106-
const err = await res.json().catch(() => ({}));
107-
throw new Error(
108-
(err as { message?: string }).message ?? `Error ${res.status} on ${path}`,
109-
);
110-
}
111-
return res.json() as Promise<T>;
65+
const { data } = await httpClient.post<{ id: string }>(
66+
"/campaigns",
67+
params,
68+
);
69+
return data;
11270
}
11371

11472
export async function enableVault(params: {
@@ -117,21 +75,29 @@ export async function enableVault(params: {
11775
enabled: boolean;
11876
callerPublicKey: string;
11977
}): Promise<{ unsignedXdr: string }> {
120-
return post("/vault/availability-for-exchange", params);
78+
const { data } = await httpClient.post<{ unsignedXdr: string }>(
79+
"/vault/availability-for-exchange",
80+
params,
81+
);
82+
return data;
12183
}
12284

12385
export async function getVaultIsEnabled(
12486
contractId: string,
12587
callerPublicKey: string,
12688
): Promise<{ enabled: boolean }> {
127-
return get(
89+
const { data } = await httpClient.get<{ enabled: boolean }>(
12890
`/vault/is-enabled?contractId=${contractId}&callerPublicKey=${callerPublicKey}`,
12991
);
92+
return data;
13093
}
13194

13295
export async function updateCampaignVaultId(
13396
campaignId: string,
13497
vaultId: string,
13598
): Promise<unknown> {
136-
return patch(`/campaigns/${campaignId}`, { vaultId });
99+
const { data } = await httpClient.patch(`/campaigns/${campaignId}`, {
100+
vaultId,
101+
});
102+
return data;
137103
}

apps/backoffice-tokenization/src/features/home/HeroSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const HeroSection = () => {
2020
project, from contract deployment to milestone execution.
2121
</p>
2222

23-
<Link href="/manage-escrows">
23+
<Link href="/campaigns">
2424
<RainbowButton variant="outline">Open App</RainbowButton>
2525
</Link>
2626
</div>

apps/backoffice-tokenization/src/features/tokens/services/token.service.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios, { AxiosInstance } from "axios";
1+
import { httpClient } from "@/lib/httpClient";
22

33
export type DeployTokenResponse = {
44
success: boolean;
@@ -13,22 +13,8 @@ export type DeployTokenParams = {
1313
};
1414

1515
export class TokenService {
16-
private readonly apiUrl: string;
17-
private readonly axios: AxiosInstance;
18-
19-
constructor() {
20-
// If NEXT_PUBLIC_API_URL is set, use it. Otherwise, use relative path /api
21-
// This allows the service to work both with external APIs and Next.js route handlers
22-
const envApiUrl = process.env.NEXT_PUBLIC_API_URL;
23-
this.apiUrl = envApiUrl && envApiUrl.trim() !== "" ? envApiUrl : "/api";
24-
25-
this.axios = axios.create({
26-
baseURL: this.apiUrl,
27-
});
28-
}
29-
3016
async deployToken(params: DeployTokenParams): Promise<DeployTokenResponse> {
31-
const response = await this.axios.post<DeployTokenResponse>("/deploy", {
17+
const response = await httpClient.post<DeployTokenResponse>("/deploy", {
3218
escrowContractId: params.escrowContractId,
3319
tokenName: params.tokenName,
3420
tokenSymbol: params.tokenSymbol,

0 commit comments

Comments
 (0)