Skip to content
Merged
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: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,5 @@ SANITY_STUDIO_PROJECT_ID=your_project_id_here
# Next.js App (client-side)
NEXT_PUBLIC_SANITY_DATASET=production
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id_here

NEXT_PUBLIC_BUNDLER_SERVER_URL=http://localhost:3001
40 changes: 34 additions & 6 deletions app/api/v1/recipients/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const POST = withRateLimit(async (request: NextRequest) => {
});

const body = await request.json();
const { name, institution, institutionCode, accountIdentifier, type } =
const { name, institution, institutionCode, accountIdentifier, type, currency } =
body;

// Validate request body
Expand Down Expand Up @@ -152,6 +152,34 @@ export const POST = withRateLimit(async (request: NextRequest) => {
);
}

const trimmedInstitutionCode = String(institutionCode).trim();
const sanitizedIdentifier = String(accountIdentifier).trim();

// Only enforce NUBAN digit-length validation for NGN recipients
if (currency === "NGN") {
const digits = sanitizedIdentifier.replace(/\D/g, "");
const requiredLen = trimmedInstitutionCode === "SAFAKEPC" ? 6 : 10;
if (digits.length !== requiredLen) {
trackApiError(
request,
"/api/v1/recipients",
"POST",
new Error("Invalid account identifier length"),
400,
);
return NextResponse.json(
{
success: false,
error:
requiredLen === 10
? "Please enter a valid 10-digit account number."
: "Please enter a valid 6-digit account number.",
},
{ status: 400 },
);
}
}

// Validate type
if (!["bank", "mobile_money"].includes(type)) {
trackApiError(
Expand Down Expand Up @@ -199,7 +227,7 @@ export const POST = withRateLimit(async (request: NextRequest) => {
}
}

// Insert recipient (upsert on unique constraint)
// Insert recipient (upsert on unique constraint) - store sanitized digits so DB has consistent format
const { data, error } = await supabaseAdmin
.from("saved_recipients")
.upsert(
Expand All @@ -208,8 +236,8 @@ export const POST = withRateLimit(async (request: NextRequest) => {
normalized_wallet_address: walletAddress,
name: name.trim(),
institution: institution.trim(),
institution_code: institutionCode.trim(),
account_identifier: accountIdentifier.trim(),
institution_code: trimmedInstitutionCode,
account_identifier: currency === "NGN" ? sanitizedIdentifier.replace(/\D/g, "") : sanitizedIdentifier,
type,
},
{
Expand Down Expand Up @@ -244,14 +272,14 @@ export const POST = withRateLimit(async (request: NextRequest) => {
const responseTime = Date.now() - startTime;
trackApiResponse("/api/v1/recipients", "POST", 200, responseTime, {
wallet_address: walletAddress,
institution_code: institutionCode,
institution_code: trimmedInstitutionCode,
type,
});

// Track business event
trackBusinessEvent("Recipient Saved", {
wallet_address: walletAddress,
institution_code: institutionCode,
institution_code: trimmedInstitutionCode,
type,
});

Expand Down
5 changes: 0 additions & 5 deletions app/api/v1/wallets/migration-status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,24 @@ export async function GET(request: NextRequest) {
.single();

if (error) {
// PGRST116 = no rows found — user has no smart wallet, not an error
if (error.code === "PGRST116") {
return NextResponse.json({
migrationCompleted: false,
status: "unknown",
hasSmartWallet: false,
});
}

// PGRST205 = table not found in schema cache — migration not applied yet
if (error.code === "PGRST205") {
console.warn(
"⚠️ Wallets table not found in schema cache. Migration may not be applied yet."
);

return NextResponse.json({
migrationCompleted: false,
status: "schema_unavailable",
hasSmartWallet: false,
error: "Database schema not ready",
});
}

console.error("Database query error:", error);
return NextResponse.json({
migrationCompleted: false,
Expand Down
4 changes: 1 addition & 3 deletions app/components/TransferForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import React, { useEffect, useState, useRef } from "react";
import { useForm } from "react-hook-form";
import { usePrivy, useWallets } from "@privy-io/react-auth";
import { useSmartWallets } from "@privy-io/react-auth/smart-wallets";
import { useShouldUseEOA, useWalletMigrationStatus } from "../hooks/useEIP7702Account";
import { useNetwork } from "../context/NetworksContext";
import { useBalance, useTokens } from "../context";
Expand Down Expand Up @@ -44,7 +43,6 @@ export const TransferForm: React.FC<{
}> = ({ onClose, onSuccess, showBackButton = false, setCurrentView }) => {
const searchParams = useSearchParams();
const { selectedNetwork } = useNetwork();
const { client } = useSmartWallets();
const { user, getAccessToken } = usePrivy();
const { wallets } = useWallets();
const shouldUseEOA = useShouldUseEOA();
Expand Down Expand Up @@ -120,12 +118,12 @@ export const TransferForm: React.FC<{
getTxExplorerLink,
error,
} = useSmartWalletTransfer({
client: client ?? null,
selectedNetwork: transferNetwork,
user,
supportedTokens: fetchedTokens,
getAccessToken,
refreshBalance,
onRequireMigration: () => setIsMigrationModalOpen(true),
});

useEffect(() => {
Expand Down
21 changes: 16 additions & 5 deletions app/components/WalletMigrationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import { AnimatePresence, motion } from "framer-motion";
import { Dialog, DialogPanel } from "@headlessui/react";
Expand All @@ -17,21 +17,32 @@ const WalletMigrationModal: React.FC<WalletMigrationModalProps> = ({
}) => {
const [showTransferModal, setShowTransferModal] = useState(false);

const handleApproveMigration = () => {
onClose();
const handleApproveMigration = (
event: React.MouseEvent<HTMLButtonElement>
) => {
// Prevent click-through where the same click immediately closes the next modal.
event.preventDefault();
event.stopPropagation();
setTimeout(() => {
setShowTransferModal(true);
}, 300);
}, 50);
};

const handleCloseTransferModal = () => {
setShowTransferModal(false);
onClose();
};

useEffect(() => {
if (!isOpen && showTransferModal) {
setShowTransferModal(false);
}
}, [isOpen, showTransferModal]);

return (
<>
<AnimatePresence>
{isOpen && (
{isOpen && !showTransferModal && (
<Dialog
key="wallet-migration-modal"
open={isOpen}
Expand Down
Loading