Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
# These are the Soroban zero-address placeholders used when mock data is on.
# Replace with real addresses before setting NEXT_PUBLIC_ENABLE_MOCK_DATA=false.
NEXT_PUBLIC_INVOICE_CONTRACT_ID=CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA
NEXT_PUBLIC_MARKETPLACE_CONTRACT_ID=CBWOAOZCOAJQH7HHZRE5BVNL2C4HRP4JCQZF3YQCQYDL5BZJRN4YGK4
NEXT_PUBLIC_MARKETPLACE_CONTRACT_ID=CBWOAOZCOAJQH7HHZRE5BVNL2C4HRP4JCQZF3YQCQYDL5BZJRN4YGK4A
NEXT_PUBLIC_TOKEN_CONTRACT_ID=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC

# ─── IPFS / Pinata ────────────────────────────────────────────────────────────
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ node_modules/
out/
build/
dist/
target/
**/target/

# env
.env
Expand Down
11 changes: 10 additions & 1 deletion app/dashboard/sme/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import { useUsdcBalance } from "@/hooks/useUsdcBalance";
import { useSuspenseQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys";
import { useMaturityReminder } from "@/hooks/useMaturityReminder";
import { useFundingReminder } from "@/hooks/useFundingReminder";
import { useRepaymentReminder } from "@/hooks/useRepaymentReminder";
import { useUIStore, useInvoiceStore } from "@/store";
import { MOCK_INVOICES } from "@/services/mockData";
import {
Expand Down Expand Up @@ -190,14 +192,17 @@ export default function SMEDashboardPage() {
);

useEffect(() => {
return queueRef.current.subscribe((snap) => {
const unsubscribe = queueRef.current.subscribe((snap) => {
setBatchItems(snap.items);
setIsBatchProcessing(snap.isRunning);
setBatchProgress(
snap.items.length === 0 ? 0 : (snap.processed / snap.items.length) * 100
);
persistBatchQueue(snap.items);
});
return () => {
unsubscribe();
};
}, []);

const myInvoices: Invoice[] =
Expand All @@ -218,6 +223,10 @@ export default function SMEDashboardPage() {
allMyInvoices.filter((invoice) => ["listed", "partially_funded", "fully_funded"].includes(invoice.status))
);

useFundingReminder(allMyInvoices);

useRepaymentReminder(allMyInvoices);

// Must run before the early return below so hook order stays stable across renders.
const { executeProtectedAction } = useVerifiedAction();

Expand Down
6 changes: 4 additions & 2 deletions app/invoice/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ export default function CreateInvoicePage() {
);

setIsUploading(false);
tempMetadataCid = result.metadataCid;
return result.unsignedXdr;
},
{
successMessage: "Invoice minted on Soroban!",
Expand All @@ -331,8 +333,8 @@ export default function CreateInvoicePage() {
}
);

if (error) {
setFileError(error);
if (txError) {
setFileError(txError);
setIsUploading(false);
}
};
Expand Down
1 change: 0 additions & 1 deletion app/marketplace/[id]/InvoiceDetailClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ import {
isTestnetUsdcFaucetEnabled,
useUsdcBalance,
} from "@/hooks/useUsdcBalance";
import { env } from "@/lib/env";
import { PrintLayout, PrintButton } from "@/components/ui/print-layout";

export default function InvoiceDetailClient({ id }: { id: string }) {
Expand Down
2 changes: 1 addition & 1 deletion app/marketplace/[id]/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default async function OpenGraphImage({ params }: Props) {
subtitle = meta.debtor?.name || subtitle;
amount = `${meta.currency} ${Number(meta.amount).toLocaleString()}`;
apr = `${invoice.terms.apr}% APR`;
risk = meta.risk_tier;
risk = meta.risk_tier || "";
due = meta.due_date ? `Due ${meta.due_date}` : "";
}
} catch {
Expand Down
2 changes: 0 additions & 2 deletions app/marketplace/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { useInvoiceStore, DEFAULT_FILTERS } from "@/store";
import { Container } from "@/components/layout/Container";
import { cn } from "@/lib/utils";
import { sanitizeQueryParam } from "@/lib/security";
import { useTranslations } from "next-intl";
import { ErrorBoundary } from "@/components/ui/error-boundary";
import { RangeSlider } from "@/components/ui/range-slider";
import { ComparisonBar } from "@/components/marketplace/ComparisonBar";
Expand Down Expand Up @@ -304,7 +303,6 @@ function Switch({
function MarketplaceContent() {
const t = useTranslations("marketplace");
const comparisonEnabled = useFeatureFlag("comparison");
const t = useTranslations("marketplace");
const router = useRouter();
const searchParams = useSearchParams();
const categoryOptions = useMemo(() => getCategoryOptions(t), [t]);
Expand Down
52 changes: 16 additions & 36 deletions app/secondary/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -356,31 +356,21 @@ export default function SecondaryMarketplacePage() {
<Clock className="h-4 w-4 text-zinc-400" />
<Select
value={tenorFilter}
onChange={(e) => setTenorFilter(e.target.value)}
onChange={(val) => setTenorFilter(val)}
options={TENOR_OPTIONS}
className="w-40 bg-zinc-950/80 border-zinc-800 text-xs"
>
{TENOR_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</Select>
/>
</div>

{/* Yield Filter */}
<div className="flex items-center gap-2">
<Percent className="h-4 w-4 text-zinc-400" />
<Select
value={yieldFilter}
onChange={(e) => setYieldFilter(e.target.value)}
onChange={(val) => setYieldFilter(val)}
options={YIELD_OPTIONS}
className="w-36 bg-zinc-950/80 border-zinc-800 text-xs"
>
{YIELD_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</Select>
/>
</div>

{/* Seller Filter */}
Expand Down Expand Up @@ -418,7 +408,7 @@ export default function SecondaryMarketplacePage() {
<SlidersHorizontal className="mr-2 h-3.5 w-3.5 text-primary" />
Filter Positions
{hasActiveFilters && (
<Badge variant="secondary" className="ml-2 bg-primary/20 text-primary text-[10px]">
<Badge variant="default" className="ml-2 bg-primary/20 text-primary text-[10px]">
Active
</Badge>
)}
Expand All @@ -432,8 +422,8 @@ export default function SecondaryMarketplacePage() {
<EmptyState
title="No Transferable Positions Found"
description="No secondary market position listings match your filter criteria. Try expanding your tenor or yield requirements."
actionText="Clear All Filters"
onAction={resetFilters}
cta={{ label: "Clear All Filters", onClick: resetFilters }}
variant="no-results"
/>
) : (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
Expand Down Expand Up @@ -540,7 +530,7 @@ export default function SecondaryMarketplacePage() {
)}

{/* Mobile Filter Bottom Sheet */}
<BottomSheet isOpen={mobileFilterOpen} onClose={() => setMobileFilterOpen(false)}>
<BottomSheet open={mobileFilterOpen} onOpenChange={setMobileFilterOpen}>
<div className="space-y-4 p-4 text-zinc-100">
<h3 className="text-base font-semibold text-white">Filter Secondary Positions</h3>

Expand All @@ -549,30 +539,20 @@ export default function SecondaryMarketplacePage() {
<label className="text-xs text-zinc-400 mb-1 block">Remaining Tenor</label>
<Select
value={tenorFilter}
onChange={(e) => setTenorFilter(e.target.value)}
onChange={(val) => setTenorFilter(val)}
options={TENOR_OPTIONS}
className="w-full bg-zinc-900 border-zinc-800 text-xs"
>
{TENOR_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</Select>
/>
</div>

<div>
<label className="text-xs text-zinc-400 mb-1 block">Minimum Yield</label>
<Select
value={yieldFilter}
onChange={(e) => setYieldFilter(e.target.value)}
onChange={(val) => setYieldFilter(val)}
options={YIELD_OPTIONS}
className="w-full bg-zinc-900 border-zinc-800 text-xs"
>
{YIELD_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</Select>
/>
</div>

<div>
Expand Down
7 changes: 7 additions & 0 deletions app/test-components/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as React from "react";
import { FileInput } from "@/components/ui/file-input";
import { RangeSlider } from "@/components/ui/range-slider";
import { DatePicker } from "@/components/ui/date-picker";
import { NotificationSettings } from "@/components/settings/NotificationSettings";

// ── FileInput fixture ────────────────────────────────────────────────────────

Expand Down Expand Up @@ -126,6 +127,12 @@ export default function TestComponentsPage() {
<FileInputFixture />
<RangeSliderFixture />
<DatePickerFixture />
<section id="notification-settings-fixture" className="space-y-4">
<h2 className="text-sm font-semibold uppercase tracking-widest text-zinc-400">
NotificationSettings
</h2>
<NotificationSettings />
</section>
</main>
);
}
1 change: 1 addition & 0 deletions components/dev/FeatureFlagPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const FLAG_DESCRIPTIONS: Record<FeatureFlag, string> = {
comparison: "Enable marketplace invoice comparison UI.",
"onboarding-tour": "Mount the guided onboarding tour overlay.",
"batch-actions": "Expose batch invoice management actions.",
"kyb-mint-gate": "Gate invoice creation behind KYB status checks.",
};

export function FeatureFlagPanel() {
Expand Down
1 change: 1 addition & 0 deletions components/development/FeatureFlagPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const FLAG_DESCRIPTIONS: Record<FeatureFlag, string> = {
comparison: "Enable marketplace invoice comparison UI.",
"onboarding-tour": "Mount the guided onboarding tour overlay.",
"batch-actions": "Expose batch invoice management actions.",
"kyb-mint-gate": "Gate invoice creation behind KYB status checks.",
};

export function FeatureFlagPanel() {
Expand Down
6 changes: 4 additions & 2 deletions components/marketplace/ComparisonBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export function ComparisonBar() {

// ─── Feature flag ────────────────────────────────────────────────────────
const isComparisonEnabled = isEnabled("comparison");
if (!isComparisonEnabled) return null;

// Resolve selected invoices from store list + token map (live indexer shape)
const invoiceIndex = [
Expand All @@ -54,6 +53,7 @@ export function ComparisonBar() {

// Hydrate comparison list from shareable URL once
useEffect(() => {
if (!isComparisonEnabled) return;
if (hydratedFromUrl.current) return;
hydratedFromUrl.current = true;

Expand All @@ -75,6 +75,7 @@ export function ComparisonBar() {

// Keep ?compare= in sync with selection for shareable URLs
useEffect(() => {
if (!isComparisonEnabled) return;
if (!hydratedFromUrl.current) return;

const params = new URLSearchParams(searchParams.toString());
Expand All @@ -92,7 +93,7 @@ export function ComparisonBar() {

const qs = params.toString();
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
}, [comparisonList, pathname, router, searchParams]);
}, [isComparisonEnabled, comparisonList, pathname, router, searchParams]);

const handleShare = async () => {
const url = new URL(window.location.href);
Expand All @@ -108,6 +109,7 @@ export function ComparisonBar() {
}
};

if (!isComparisonEnabled) return null;
if (comparisonList.length === 0) return null;

// ─── Mobile: show only count + compare button ──────────────────────────
Expand Down
Loading