Skip to content

Commit 9da6d60

Browse files
authored
Merge pull request #410 from nursetechie/feat/unified-contributions
2 parents f42a4df + 3f23c95 commit 9da6d60

8 files changed

Lines changed: 50 additions & 5 deletions

File tree

app/(merchant)/dashboard/page.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ export default function DashboardPage() {
295295
{payments.slice(0, 3).map((link) => {
296296
const baseUrl = process.env.NEXT_PUBLIC_API_URL ?? '';
297297
const linkUrl = `${baseUrl}/pay/${link.id}`;
298+
const clicks = link.clicks ?? 0;
299+
const converted = link.converted ?? 0;
300+
const conversionRate = clicks > 0 ? (converted / clicks) * 100 : 0;
301+
const rateLabel = clicks > 0 ? `${conversionRate.toFixed(0)}%` : 'No data';
302+
298303
return (
299304
<Link
300305
key={link.id}
@@ -309,9 +314,11 @@ export default function DashboardPage() {
309314
<p className="text-xs text-muted-foreground font-mono truncate">{linkUrl}</p>
310315
<div className="flex items-center gap-2 mt-1.5">
311316
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
312-
<div className="h-full bg-amber-400 rounded-full" style={{ width: '50%' }} />
317+
{clicks > 0 && (
318+
<div className="h-full bg-amber-400 rounded-full" style={{ width: `${conversionRate}%` }} />
319+
)}
313320
</div>
314-
<span className="text-xs text-muted-foreground font-medium"></span>
321+
<span className="text-xs text-muted-foreground font-medium">{rateLabel}</span>
315322
</div>
316323
</div>
317324
<div className="flex flex-col items-end gap-1 flex-shrink-0">

app/api/auth/session/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,9 @@ export async function DELETE() {
7575
'Set-Cookie',
7676
`csrf_token=; Path=/; Max-Age=0; SameSite=Strict${secureFlag}`
7777
);
78+
res.headers.append(
79+
'Set-Cookie',
80+
`merchant_onboarded=; Path=/; Max-Age=0; SameSite=Lax${secureFlag}`
81+
);
7882
return res;
7983
}

app/api/contact/route.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,19 @@ export async function POST(req: Request) {
9696

9797
// 2. Enforce Rate Limiting
9898
if (!checkRateLimit(ip)) {
99+
const record = rateLimitMap.get(ip);
100+
const oldestTimestamp = record && record.timestamps.length > 0 ? record.timestamps[0] : Date.now();
101+
const resetTimeMs = oldestTimestamp + 3600000;
102+
const remainingSeconds = Math.max(0, Math.ceil((resetTimeMs - Date.now()) / 1000));
103+
99104
return NextResponse.json(
100105
{ error: "Too many submissions. Please try again in an hour." },
101-
{ status: 429 }
106+
{
107+
status: 429,
108+
headers: {
109+
"Retry-After": remainingSeconds.toString(),
110+
},
111+
}
102112
);
103113
}
104114

app/onboarding/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ export default function OnboardingPage() {
103103
webhookUrl: data.webhookUrl || null,
104104
});
105105
localStorage.setItem("onboardingCompleted", "true");
106+
const secureFlag = process.env.NODE_ENV === 'production' ? '; Secure' : '';
107+
document.cookie = `merchant_onboarded=true; Path=/; SameSite=Lax; Max-Age=86400${secureFlag}`;
106108
notify.success("Your merchant profile is ready!");
107109
router.push("/dashboard");
108110
} catch (error) {

app/pay/[linkId]/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ import { WalletModalFallback } from "@/components/wallet/WalletModalFallback";
3131
import { WalletModalErrorBoundary } from "@/components/wallet/WalletModalErrorBoundary";
3232
import { QRCodeModal } from "@/components/payments/QRCode";
3333

34+
function hexToUint8Array(hexString: string): Uint8Array {
35+
const cleanHex = hexString.startsWith('0x') ? hexString.slice(2) : hexString;
36+
const len = cleanHex.length;
37+
const result = new Uint8Array(len / 2);
38+
for (let i = 0; i < len; i += 2) {
39+
result[i / 2] = parseInt(cleanHex.substring(i, i + 2), 16);
40+
}
41+
return result;
42+
}
43+
3444
export default function PaymentLinkPage() {
3545
const router = useRouter();
3646
const { isConnected, connect, address } = useWalletStore();
@@ -132,7 +142,7 @@ export default function PaymentLinkPage() {
132142
new Contract(contractId).call(
133143
"store_payment_reference",
134144
nativeToScVal(merchantAddress, { type: "address" }),
135-
nativeToScVal(Buffer.from(referenceHex, "hex")),
145+
nativeToScVal(hexToUint8Array(referenceHex)),
136146
nativeToScVal(stroopAmount, { type: "i128" }),
137147
),
138148
)

lib/api/hooks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface ApiPayment {
2121
stellarOpId?: string | null;
2222
url?: string;
2323
clicks?: number;
24+
converted?: number;
2425
}
2526

2627
export interface ApiSettlement {

lib/hooks/useLogin.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export function useLogin() {
5151
if (meRes.ok) {
5252
const merchantData = await meRes.json();
5353
if (merchantData.name === 'My Business') {
54+
const secureFlag = process.env.NODE_ENV === 'production' ? '; Secure' : '';
55+
document.cookie = `merchant_onboarded=false; Path=/; SameSite=Lax; Max-Age=86400${secureFlag}`;
5456
router.push('/onboarding');
5557
return;
5658
}
@@ -59,6 +61,8 @@ export function useLogin() {
5961
// ignore
6062
}
6163

64+
const secureFlag = process.env.NODE_ENV === 'production' ? '; Secure' : '';
65+
document.cookie = `merchant_onboarded=true; Path=/; SameSite=Lax; Max-Age=86400${secureFlag}`;
6266
router.push(user.role === 'admin' ? '/overview' : '/dashboard');
6367
}, [apiBase, login, router, success, error]);
6468

middleware.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,20 @@ export function middleware(request: NextRequest) {
4949
return NextResponse.redirect(new URL('/auth/login', request.url));
5050
}
5151

52+
// Redirect onboarded merchants away from onboarding page
53+
const isOnboarded = request.cookies.get('merchant_onboarded')?.value === 'true';
54+
if (request.nextUrl.pathname === '/onboarding' && isOnboarded) {
55+
return NextResponse.redirect(new URL('/dashboard', request.url));
56+
}
57+
5258
// Role-based protection
5359
if (isAdminRoute && role !== 'admin') {
5460
return NextResponse.redirect(new URL('/dashboard', request.url)); // redirect merchants from admin
5561
}
5662

5763
// Protect merchant routes from admins
58-
const isMerchantRoute = request.nextUrl.pathname === '/dashboard' ||
64+
const isMerchantRoute = request.nextUrl.pathname === '/onboarding' ||
65+
request.nextUrl.pathname === '/dashboard' ||
5966
request.nextUrl.pathname.startsWith('/payments') ||
6067
request.nextUrl.pathname === '/transactions' ||
6168
request.nextUrl.pathname === '/settlement' ||

0 commit comments

Comments
 (0)