diff --git a/src/app/api/invoices/[id]/route.ts b/src/app/api/invoices/[id]/route.ts
index 8a1c898..db47ab0 100644
--- a/src/app/api/invoices/[id]/route.ts
+++ b/src/app/api/invoices/[id]/route.ts
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
+import { splitClient } from "@/lib/stellar";
import { safeParseSplitMeta, type SplitMetaInput } from "@/lib/splitMetaSchema";
interface SplitMetaStore {
@@ -13,6 +14,20 @@ export async function PATCH(
) {
try {
const invoiceId = params.id;
+ const walletPublicKey = request.headers.get("x-wallet-public-key");
+
+ if (!walletPublicKey) {
+ return NextResponse.json({ error: "Missing wallet public key" }, { status: 403 });
+ }
+
+ const invoice = await splitClient.getInvoice(invoiceId);
+ const isCreator = invoice.creator === walletPublicKey;
+ const isRecipient = invoice.recipients.some((recipient) => recipient.address === walletPublicKey);
+
+ if (!isCreator && !isRecipient) {
+ return NextResponse.json({ error: "Not authorized for this invoice" }, { status: 403 });
+ }
+
const rawBody = await request.json();
const { splitMeta } = rawBody as { splitMeta?: unknown };
diff --git a/src/app/invoice/[id]/page.tsx b/src/app/invoice/[id]/page.tsx
index 34f84dc..a01e9a0 100644
--- a/src/app/invoice/[id]/page.tsx
+++ b/src/app/invoice/[id]/page.tsx
@@ -34,7 +34,6 @@ import SuccessAnimation from "@/components/SuccessAnimation";
import RecipientPayoutTracker from "@/components/RecipientPayoutTracker";
import RecipientListSkeleton from "@/components/invoice/RecipientListSkeleton";
import CloneLineageTree from "@/components/CloneLineageTree";
-import TransferOwnershipModal from "@/components/TransferOwnershipModal";
import StellarErrorBoundary from "@/components/error/StellarErrorBoundary";
import { useStellarQuery } from "@/hooks/useStellarQuery";
import { useRecentInvoices } from "@/hooks/useRecentInvoices";
@@ -357,6 +356,7 @@ export default function InvoiceDetailPage({ params }: Props) {
const total = invoice
? invoice.recipients.reduce((s, r) => s + r.amount, 0n)
: 0n;
+ const role = useInvoiceRole(invoice, publicKey);
const handlePay = async (e: React.FormEvent) => {
e.preventDefault();
@@ -443,6 +443,12 @@ export default function InvoiceDetailPage({ params }: Props) {
if (!invoice) return null;
+ const isCreator = role === "creator";
+ const isRecipient = role === "recipient";
+ const canAct = isCreator || isRecipient;
+ const recipientShare = publicKey
+ ? invoice.recipients.find((recipient) => recipient.address === publicKey)
+ : undefined;
const remaining = total - invoice.funded;
const status = statusConfig[invoice.status] || { label: invoice.status, color: "bg-gray-500", icon: "⌛" };
@@ -515,7 +521,7 @@ export default function InvoiceDetailPage({ params }: Props) {
Retroactive
)}
-
+ {canAct && }
@@ -565,16 +571,18 @@ export default function InvoiceDetailPage({ params }: Props) {
{pushStatus === "active" ? "Notifications active" : "Notifications off"}
)}
-
- {(invoice as any).confidential && (
+ {canAct && (
+
+ )}
+ {isRecipient && (invoice as any).confidential && (
)}
-
-
- {invoice.status === "Pending" && publicKey === invoice.creator && (
+ {canAct && (
+
+ )}
+ {isCreator && (
+
+ )}
+ {invoice.status === "Pending" && isCreator && (
diff --git a/src/components/AuditLogTable.tsx b/src/components/AuditLogTable.tsx
index ac68fee..00bf603 100644
--- a/src/components/AuditLogTable.tsx
+++ b/src/components/AuditLogTable.tsx
@@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import { splitClient } from "@/lib/stellar";
import { truncateAddress } from "@stellar-split/sdk";
+import RelativeTime from "@/components/ui/RelativeTime";
import {
buildInvoiceArchive,
generateArchiveFilename,
@@ -108,22 +109,6 @@ export default function AuditLogTable({ invoiceId, invoice }: Props) {
const startIdx = (currentPage - 1) * ENTRIES_PER_PAGE;
const paginatedEntries = entries.slice(startIdx, startIdx + ENTRIES_PER_PAGE);
- const formatTimestamp = (timestamp: number): string => {
- try {
- return new Intl.DateTimeFormat("en-US", {
- year: "numeric",
- month: "short",
- day: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- timeZoneName: "short",
- }).format(new Date(timestamp * 1000));
- } catch {
- return new Date(timestamp * 1000).toISOString();
- }
- };
-
return (
@@ -148,7 +133,7 @@ export default function AuditLogTable({ invoiceId, invoice }: Props) {
{truncateAddress(entry.actor)}
- {formatTimestamp(entry.timestamp)}
+
|
))}
diff --git a/src/components/CommentSection.tsx b/src/components/CommentSection.tsx
index 0c32a40..33a5758 100644
--- a/src/components/CommentSection.tsx
+++ b/src/components/CommentSection.tsx
@@ -2,6 +2,7 @@
import { useEffect, useRef, useState } from "react";
import { parseMentions, notifyMention } from "@/lib/notifications";
+import RelativeTime from "@/components/ui/RelativeTime";
interface Comment {
id: string;
@@ -47,14 +48,6 @@ function deleteComment(id: string) {
);
}
-function relativeTime(timestamp: number): string {
- const diff = (Date.now() - timestamp) / 1000;
- if (diff < 60) return "just now";
- if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
- if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
- return `${Math.floor(diff / 86400)} days ago`;
-}
-
/** Stellar address pattern — must match parseMentions regex. */
const MENTION_SPLIT_RE = /(\bG[A-Z0-9]{55}\b)/g;
const MENTION_TEST_RE = /^G[A-Z0-9]{55}$/;
@@ -137,7 +130,9 @@ export default function CommentSection({ invoiceId, walletAddress }: Props) {
>
{renderCommentText(c.text)}
-
{relativeTime(c.timestamp)}
+
+
+
{evt.description}
{evt.actor && (
diff --git a/src/components/StatusTimeline.tsx b/src/components/StatusTimeline.tsx
index d9e5533..5856eb9 100644
--- a/src/components/StatusTimeline.tsx
+++ b/src/components/StatusTimeline.tsx
@@ -1,6 +1,7 @@
"use client";
import type { Invoice } from "@stellar-split/sdk";
+import RelativeTime from "@/components/ui/RelativeTime";
interface TimelineEvent {
key: string;
@@ -10,18 +11,6 @@ interface TimelineEvent {
actor?: string;
}
-function relativeTime(ts: number): string {
- const diff = Math.floor((Date.now() / 1000) - ts);
- if (diff < 60) return "just now";
- if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
- if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
- return `${Math.floor(diff / 86400)}d ago`;
-}
-
-function absoluteTime(ts: number): string {
- return new Date(ts * 1000).toLocaleString();
-}
-
function truncate(addr: string) {
return addr.length > 12 ? `${addr.slice(0, 6)}…${addr.slice(-4)}` : addr;
}
@@ -164,11 +153,8 @@ export default function StatusTimeline({ invoice, total }: Props) {
)}
{ev.timestamp ? (
-
- {relativeTime(ev.timestamp)}
+
+
) : (
timestamp pending
diff --git a/src/components/VersionHistory.tsx b/src/components/VersionHistory.tsx
index d70b102..decb212 100644
--- a/src/components/VersionHistory.tsx
+++ b/src/components/VersionHistory.tsx
@@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import { splitClient } from "@/lib/stellar";
import { truncateAddress } from "@stellar-split/sdk";
+import RelativeTime from "@/components/ui/RelativeTime";
interface HistoryEntry {
action: string;
@@ -117,13 +118,7 @@ export default function VersionHistory({ invoiceId }: Props) {
{entry.action}
- {new Intl.DateTimeFormat("en-US", {
- year: "numeric",
- month: "short",
- day: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- }).format(new Date(entry.timestamp * 1000))}
+
{/* Diff display */}
diff --git a/src/components/invoice/CommentBubble.tsx b/src/components/invoice/CommentBubble.tsx
index 90d25d3..8eef300 100644
--- a/src/components/invoice/CommentBubble.tsx
+++ b/src/components/invoice/CommentBubble.tsx
@@ -5,6 +5,7 @@ import remarkGfm from "remark-gfm";
import { truncateAddress } from "@stellar-split/sdk";
import ReactionBar from "./ReactionBar";
import type { AllowedEmoji } from "@/lib/commentStore";
+import RelativeTime from "@/components/ui/RelativeTime";
export interface CommentWithReactions {
id: string;
@@ -24,14 +25,6 @@ interface Props {
reactionsDisabled?: boolean;
}
-function relativeTime(timestamp: number): string {
- const diff = (Date.now() - timestamp) / 1000;
- if (diff < 60) return "just now";
- if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
- if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
- return `${Math.floor(diff / 86400)}d ago`;
-}
-
export default function CommentBubble({
comment,
canDelete,
@@ -55,7 +48,7 @@ export default function CommentBubble({
{truncateAddress(comment.authorAddress)}
-
{relativeTime(comment.createdAt)}
+
{canDelete && (