Skip to content

Conversation

@Dprof-in-tech
Copy link
Collaborator

@Dprof-in-tech Dprof-in-tech commented Nov 11, 2025

Description

This pull request introduces a comprehensive KYC (Know Your Customer) and phone verification flow, including new API endpoints for phone OTP (One-Time Password) verification, SmileID biometric verification, and KYC status/transaction summaries. It also updates the frontend modal to support biometric capture and phone verification, and adds configuration for third-party phone verification providers.

New KYC and Phone Verification APIs:

  • Added API endpoints for sending OTPs, verifying OTPs, checking phone verification status, submitting SmileID biometric data, checking KYC status, and summarizing user transactions. These endpoints handle validation, error tracking, analytics, and database updates for user KYC profiles. [1] [2] [3] [4] [5] [6]

Frontend KYC Modal Enhancements:

  • Updated KycModal to support a new biometric capture step using SmileID web components, including dynamic import and state management for camera and signature data. [1] [2] [3]
  • Integrated new aggregator API methods for submitting SmileID data and fetching KYC status.

Third-Party Provider Configuration:

  • Added environment variable placeholders for Termii and Twilio phone verification services in .env.example, preparing the app for international and African phone number support.

Aggregator API Utilities:

  • Implemented submitSmileIDData utility for submitting biometric KYC data to the backend, with analytics and error tracking.

New API Endpoints for KYC and Phone Verification

Frontend KYC Modal and Aggregator Integration

  • app/components/KycModal.tsx: Added biometric capture step, SmileID web component integration, and state for signature/camera. Updated to use new aggregator API methods. [1] [2] [3]
  • app/api/aggregator.ts: Added submitSmileIDData utility for submitting biometric data and tracking analytics.

Configuration for Third-Party Providers

  • .env.example: Added Termii and Twilio API key placeholders for phone verification services.

References

closes #281
closes #282
closes #283

Testing

Here's a loom video recording of what this looks like.

Loom recording of the kyc flow

  • This change adds test coverage for new/changed/fixed functionality

Checklist

  • I have added documentation and tests for new/changed functionality in this PR
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main

By submitting a PR, I agree to Paycrest's Contributor Code of Conduct and Contribution Guide.

Summary by CodeRabbit

  • New Features

    • Phone OTP verification modal with regional provider routing, resend/attempt limits, and smooth OTP UX
    • In-app identity capture and biometric KYC with multi-step capture/ID entry and camera-based flow
    • Transaction limits UI showing daily/monthly spend, progress, and prompts to verify/upgrade
    • Profile drawer exposing KYC status, quick actions, and account utilities
  • Infrastructure

    • New server endpoints for KYC, phone OTP, status, and transaction summaries; phone/SmileID integrations, DB migration and env var placeholders for verification services

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 11, 2025

Walkthrough

Adds tiered KYC and phone verification: new Supabase KYC table and migration, server APIs for OTP and SmileID KYC, provider routing (Termii/Twilio), client hooks and modals (phone, limits, profile, KYC capture), country utilities, and related dependencies.

Changes

Cohort / File(s) Summary
Env & migration
.\.env.example, supabase/migrations/create_user_kyc_profiles.sql
New env placeholders for Termii/Twilio/SmileID; SQL migration adding user_kyc_profiles table, indexes, constraints, defaults, and updated_at trigger.
Phone verification & library
app/api/phone/send-otp/route.ts, app/api/phone/verify-otp/route.ts, app/lib/phone-verification.ts
Rate-limited send/verify OTP endpoints, phone validation/formatting, Termii/Twilio routing, OTP generation, DB upsert, attempts/expiry handling, and provider integration.
SmileID integration & aggregator
app/api/kyc/smile-id/route.ts, app/lib/smileID.ts, app/api/aggregator.ts, app/types.ts
Server-side SmileID job submission helper, job-type logic, POST KYC route validating input and persisting job refs, aggregator method submitSmileIDData, and SmileIDSubmissionResponse type.
KYC status & transactions APIs
app/api/kyc/status/route.ts, app/api/kyc/transaction-summary/route.ts
GET endpoints returning KYC status and transaction summaries (daily/monthly spent) with analytics, auth checks, and error handling.
Client hooks & flow guards
app/hooks/useKYCStatus.ts, app/hooks/useSwapButton.ts, app/pages/TransactionForm.tsx
New useKYCStatus hook (tiers, limits, transaction summary, in-memory fetch guards), updated swap/button logic to open TransactionLimitModal, and client-side pre-checks blocking transactions over tier limits.
UI: Modals & profile
app/components/PhoneVerificationModal.tsx, app/components/KycModal.tsx, app/components/TransactionLimitModal.tsx, app/components/ProfileDrawer.tsx, app/components/SettingsDropdown.tsx, app/components/index.ts
Added PhoneVerificationModal, TransactionLimitModal, ProfileDrawer; KycModal replaced QR flow with SmileID capture SDK; Settings menu wired to ProfileDrawer; index re-exports new modals.
Country utilities
app/lib/countries.ts
New country fetcher with caching, fallback dataset, popular-country helper, and search utility for pickers.
Supabase client export
app/lib/supabase.ts
Exports supabaseUrl constant from env.
Middleware & deps
middleware.ts, package.json
Middleware matcher expanded for new routes; dependencies added: @smileid/web-components, libphonenumber-js, smile-identity-core, twilio.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as Noblocks App (client)
    participant API as Next.js API
    participant DB as Supabase
    participant SMS as Termii/Twilio
    Note over Client,API: Phone verification flow
    User->>Client: Enter name & phone, request OTP
    Client->>API: POST /api/phone/send-otp {phone, wallet}
    API->>DB: Upsert user_kyc_profiles (otp_code, expires_at, provider)
    API->>SMS: Send OTP via chosen provider
    SMS-->>API: Provider response
    API-->>Client: {success, provider, intlPhone}
    User->>Client: Submit OTP code
    Client->>API: POST /api/phone/verify-otp {phone, otp, wallet}
    API->>DB: Fetch profile by wallet+phone, validate otp/expiry/attempts
    alt OTP valid
        DB-->>API: Update verified=true, verified_at, tier=1 (if applicable)
        API-->>Client: 200 {verified:true, phoneNumber}
    else OTP invalid/expired/locked
        API-->>Client: 4xx error (attempts/expired)
    end
Loading
sequenceDiagram
    actor User
    participant Client as Noblocks App (client)
    participant KycAPI as /api/kyc/smile-id
    participant Smile as SmileID Server
    participant DB as Supabase
    Note over Client,KycAPI: In-app SmileID capture and submission
    User->>Client: Start Full KYC -> capture images + id_info via smart-camera-web
    Client->>KycAPI: POST /api/kyc/smile-id {images, id_info, wallet} (Bearer token)
    KycAPI->>Smile: submitSmileIDJob(...) (server-side)
    Smile-->>KycAPI: {job_id, status, details}
    alt submission success
        KycAPI->>DB: Update user_kyc_profiles (platform array, id info, job_id, verified/tier if applicable)
        KycAPI-->>Client: {status: "success", jobId, smileIdResponse}
    else error
        KycAPI-->>Client: 4xx/5xx error with diagnostics
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Files/areas to focus on:
    • OTP race conditions and atomic attempts/expiry handling (app/api/phone/*, app/lib/phone-verification.ts).
    • SmileID job-type determination, dynamic import, env var validation, and error/logging (app/lib/smileID.ts, app/api/kyc/smile-id/route.ts, app/api/aggregator.ts).
    • Supabase upsert/merge semantics for platform JSONB and conditional tier updates (app/api/kyc/*, migration SQL).
    • useKYCStatus fetch-guard correctness and client/server limit consistency (app/hooks/useKYCStatus.ts, app/pages/TransactionForm.tsx).
    • Client UI flow changes and nested modal interactions (app/components/*).

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • chibie
  • jeremy0x

Poem

🐇 I hopped in with a tiny code-kit,
Sent a pin, took a selfie, then sit—
SmileID blinked and jobs took flight,
Profiles grew and tiers shone bright.
A carrot toast: verified tonight! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: adding Smile ID KYC submission functionality and integrating the smart camera component in KycModal.
Description check ✅ Passed The description is comprehensive, covering all major changes including new API endpoints, frontend updates, and environment configuration with clear references and testing documentation.
Linked Issues check ✅ Passed The PR successfully implements core requirements from issues #281 (tiered KYC flow with limits), #282 (phone verification via Termii/Twilio), and #283 (Smile ID Web SDK integration).
Out of Scope Changes check ✅ Passed All code changes are directly aligned with the KYC and phone verification objectives; no unrelated modifications detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-use-smileID-web-sdk-for-in-app-verification

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Implemented TransactionLimitModal to manage user transaction limits based on KYC status.
- Integrated phone verification and KYC status hooks to enhance user verification process.
- Updated SettingsDropdown to include a Profile option and handle profile drawer state.
- Created useKYCStatus and usePhoneVerification hooks for managing KYC and phone verification states.
- Added country fetching utility and phone number validation logic.
- Introduced SmileID integration for selfie verification.
- Enhanced Supabase schema to support user KYC profiles and verification processes.
- Updated TransactionForm to check transaction limits before processing swaps.
- Added necessary dependencies for phone verification and KYC functionalities.
@Dprof-in-tech Dprof-in-tech marked this pull request as ready for review November 20, 2025 14:18
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/hooks/useSwapButton.ts (1)

81-118: Fix inverted Fund Wallet condition and align KYC behaviour with button text/state

There are a couple of behavioural bugs in the updated logic:

  1. Fund wallet condition is inverted
if (!hasInsufficientBalance && !isInjectedWallet && authenticated) {
  return handleFundWallet;
}

This triggers handleFundWallet when the user has sufficient balance (and the button text in that case is usually "Swap"), which is clearly not intended. It should gate funding on insufficient balance.

  1. KYC vs text mismatch for insufficient & unverified
if (hasInsufficientBalance && !isUserVerified && (authenticated || isInjectedWallet)) {
  return setIsLimitModalOpen;
}

For an authenticated, non‑injected user with insufficient balance and !isUserVerified, buttonText is "Fund wallet" (because of the earlier text branch), but the action now opens the limit/KYC modal. That’s confusing UX and contradicts the label.

  1. Injected wallet + insufficient balance never reaches KYC

isEnabled still short‑circuits with:

if (isInjectedWallet && hasInsufficientBalance) {
  return false;
}

So for injected wallets with insufficient balance, the button is disabled and the new KYC branch in buttonAction is never called, even though the condition includes isInjectedWallet.

A minimal fix for the worst regression (1) and to make the order of actions clearer could be:

   const buttonAction = (
     handleSwap: () => void,
     login: () => void,
     handleFundWallet: () => void,
     setIsLimitModalOpen: () => void,
     isUserVerified: boolean,
   ) => {
     if (!authenticated && !isInjectedWallet) {
       return login;
     }
-    if (!hasInsufficientBalance && !isInjectedWallet && authenticated) {
-      return handleFundWallet;
-    }
-    if (hasInsufficientBalance && !isUserVerified && (authenticated || isInjectedWallet)) {
-      return setIsLimitModalOpen;
-    }
-    return handleSwap;
+    if (hasInsufficientBalance) {
+      if (!isUserVerified && (authenticated || isInjectedWallet)) {
+        return setIsLimitModalOpen;
+      }
+      if (!isInjectedWallet && authenticated) {
+        return handleFundWallet;
+      }
+    }
+    return handleSwap;
   };

Follow‑ups (recommended, not strictly required for correctness of this fix):

  • Update buttonText so that the label matches the action (e.g. show "Get started" or similar when setIsLimitModalOpen will fire, and only show "Fund wallet" when the actual action is handleFundWallet).
  • Revisit isEnabled for injected wallets with insufficient balance if you really want KYC to be reachable in that scenario.
🧹 Nitpick comments (17)
app/api/kyc/transaction-summary/route.ts (1)

22-60: Clarify period semantics and consider explicit UTC handling

The aggregation logic is correct, but two small points:

  • The comment says “last 30 days” while the query uses monthStart (start of current month). Either update the comment or change the filter to a rolling 30‑day window to avoid confusion.
  • today and monthStart are based on the server’s local timezone while created_at is likely stored as UTC. That’s usually fine, but if you care about strict UTC month/day boundaries, consider constructing these dates in UTC or documenting the intended behaviour.
app/api/kyc/status/route.ts (1)

22-45: Handle Supabase errors explicitly and tighten tier fallback logic

Two things to consider here:

  1. Supabase error handling
    supabaseAdmin.from(...).single() can return an error (e.g., network issue, Postgres error). Right now you ignore it, which means a DB failure looks like “no profile found” and you still return 200 with tier 0. That makes observability harder and can block verified users due to transient backend issues.

    Suggestion: capture { data: kycProfile, error } and, if error is truthy and not a simple “no rows” case, log/track it and return a 500, similar to the transaction-summary route.

  2. Tier fallback check
    The comment says “use database tier if available, otherwise calculate”, but:

    let tier: 0 | 1 | 2 = kycProfile?.tier || 0;
    if (!kycProfile?.tier) {
      if (fullKYCVerified) tier = 2;
      else if (phoneVerified) tier = 1;
    }

    Because 0 is falsy, a stored tier = 0 is treated as “not set”, and the fallback can overwrite it based on verification flags. If you intend to respect an explicit 0, change the condition to a null/undefined check, e.g. if (kycProfile?.tier == null) { ... }.

app/lib/countries.ts (1)

15-104: Country lookup and caching logic looks solid

The fetch → transform → cache flow, plus the hardcoded fallback list, gives you a nice balance of UX and resilience if the REST Countries API is down. searchCountries and getPopularCountries are straightforward and usable by UI components.

If this becomes performance‑critical later, you might consider adding a simple TTL to countriesCache (so long‑lived server processes can refresh occasionally), but what you have is perfectly acceptable for now.

app/components/TransactionLimitModal.tsx (2)

210-215: Wire setIsUserVerified to refresh KYC state and close parent on success

Here setIsUserVerified={() => { }} is a no-op, so a successful Smile ID KYC flow in KycModal won’t update this component’s KYC state or close the TransactionLimitModal. The user has to manually close and reopen to see updated limits.

You can use the success callback to refresh status and close the parent when verification succeeds:

-                        <KycModal
-                            setIsKycModalOpen={setIsKycModalOpen}
-                            setIsUserVerified={() => { }}
-                        />
+                        <KycModal
+                            setIsKycModalOpen={setIsKycModalOpen}
+                            setIsUserVerified={async (verified) => {
+                                if (verified) {
+                                    await refreshStatus();
+                                    onClose();
+                                }
+                            }}
+                        />

This keeps the limit UI in sync with backend KYC status and matches the expectation that the enclosing flow completes when verification passes.


54-59: Simplify tier check in auto-open effect

tier is typed as 0 | 1 | 2 and initialized to 0, so tier === undefined can never be true. The condition can be simplified for clarity:

-    useEffect(() => {
-        if (isOpen && !isLoading && (tier < 1 || tier === undefined)) {
-            setIsPhoneModalOpen(true);
-        }
-    }, [isOpen, isLoading, tier]);
+    useEffect(() => {
+        if (isOpen && !isLoading && tier < 1) {
+            setIsPhoneModalOpen(true);
+        }
+    }, [isOpen, isLoading, tier]);
app/components/ProfileDrawer.tsx (1)

42-47: Future‑proof monthlyProgress against zero monthly limits

Right now monthlyProgress is computed as:

const monthlyProgress = (transactionSummary.monthlySpent / currentLimits.monthly) * 100;

and the “Current Tier Status” card only renders when tier >= 1, where currentLimits.monthly is non‑zero, so this is safe today. If KYC tiers or getCurrentLimits are ever extended to allow a non‑zero tier with a 0 monthly limit, this could become a division‑by‑zero and produce NaN/Infinity.

Optional: add a small guard for robustness, e.g.:

const monthlyProgress =
  currentLimits.monthly > 0
    ? (transactionSummary.monthlySpent / currentLimits.monthly) * 100
    : 0;

Overall, the drawer’s integration with useKYCStatus and TransactionLimitModal looks good.

app/lib/supabase.ts (1)

3-22: Align env validation for NEXT_PUBLIC_SUPABASE_URL with other Supabase envs

supabaseAdmin throws early if SUPABASE_URL/SUPABASE_SERVICE_ROLE_KEY are missing, but supabaseUrl can be undefined if NEXT_PUBLIC_SUPABASE_URL is misconfigured, pushing the failure to wherever it’s used.

To keep failures explicit and consistent, consider adding a simple guard:

 export const supabaseAdmin = createClient(
   process.env.SUPABASE_URL,
   process.env.SUPABASE_SERVICE_ROLE_KEY,
   {
     auth: {
       autoRefreshToken: false,
       persistSession: false,
     },
   }
 );
 
-export const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+export const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+if (!supabaseUrl) {
+  throw new Error("Missing env.NEXT_PUBLIC_SUPABASE_URL");
+}
app/api/phone/status/route.ts (1)

22-40: Consider logging Supabase fetchError separately for better observability

Right now, if the Supabase query returns a fetchError, the route still returns 200 with verified: false and no analytics entry for the DB error (only a generic trackApiResponse above).

If distinguishing “no record” from “DB failure” would help debugging, you could log/track the fetchError explicitly before falling back to the non-verified response, for example:

if (fetchError) {
  console.error("Supabase phone status fetch error:", fetchError);
  // optionally trackApiError(...) here
}

if (!verification) {
  return NextResponse.json({
    success: true,
    verified: false,
    phoneNumber: null,
    verifiedAt: null,
    provider: null,
  });
}

This keeps the user-facing behavior the same while making operational issues easier to spot.

app/api/kyc/smile-id/route.ts (1)

71-79: Revisit storing raw images payload in image_links

image_links is being set to JSON.stringify(images). Given this is biometric KYC data, storing full image payloads directly in user_kyc_profiles can:

  • Make the table heavy and slower to query.
  • Increase exposure of sensitive content in case of accidental wide queries or logs.

If the column is intended to store references (e.g. URLs or IDs managed by SmileID or object storage), consider persisting only those references instead of the raw images array. If regulatory requirements mandate storing images, it may be cleaner to keep them in a dedicated, access-controlled table or storage bucket and store minimal metadata here.

app/pages/TransactionForm.tsx (2)

107-108: Clarify units for usdAmount used in canTransact / limit checks

In handleSwap, you compute:

const formData = getValues();
const usdAmount = formData.amountReceived || 0;

// Check transaction limits based on KYC tier
const limitCheck = canTransact(usdAmount);

The comment says “Calculate the USD amount”, but amountReceived is the fiat amount in the selected payout currency, not necessarily USD. Since useKYCStatus.canTransact and the KYC tiers appear to be expressed in a single currency (displayed with $ in TransactionLimitModal), this is slightly misleading and could cause incorrect checks if limits are indeed USD-based.

Consider either:

  • Renaming the variable to something currency-agnostic (transactionAmount) if limits are intended to be in the payout currency, or
  • Converting amountReceived to the canonical currency used by your KYC limits (likely USD) before calling canTransact, to keep the comparison consistent.

This will make the behavior clearer and reduce future confusion around what units the limit checker expects.

Also applies to: 460-477


853-857: TransactionLimitModal integration and button callback look correct

Rendering TransactionLimitModal with isOpen, onClose, and transactionAmount={blockedTransactionAmount}, and passing () => setIsLimitModalOpen(true) into buttonAction keeps all “limit exceeded / need KYC upgrade” cases funneled through a single modal.

One small improvement you might consider later: when opening the modal from buttonAction (not handleSwap), pass the current intended receive amount into blockedTransactionAmount as well, so the modal can consistently reflect the attempted transaction size.

Also applies to: 887-888

app/hooks/usePhoneVerification.ts (1)

40-84: Tighten useCallback dependencies to avoid future stale closures

checkVerificationStatus closes over fetchGuards and guardKey but only depends on walletAddress. This works today because both derive from walletAddress/window, but it can become brittle if the guard key computation changes.

To keep this resilient and satisfy react-hooks/exhaustive-deps, consider:

-  const checkVerificationStatus = useCallback(async () => {
+  const checkVerificationStatus = useCallback(async () => {
     // ...
-  }, [walletAddress]);
+  }, [walletAddress, guardKey, fetchGuards]);

This ensures the guard logic always reflects the latest key/map while preserving behavior.

app/api/phone/verify-otp/route.ts (1)

51-65: Consider tracking analytics for expiry and max-attempts branches

The OTP expiry and “maximum attempts exceeded” paths return 400/429 without calling trackApiError or trackApiResponse. If you rely on analytics for monitoring abuse or UX friction, instrumenting these branches similarly to other error paths would make the metrics more complete.

This is non-blocking but worth aligning for observability.

app/api/phone/send-otp/route.ts (1)

28-35: Optional: add analytics for invalid phone number format

When !validation.isValid, the route returns a 400 but doesn’t log via trackApiError or trackApiResponse. If you care about measuring validation failures (e.g. country code/format mistakes), consider adding analytics here similar to the “missing required fields” case.

Non-blocking, just for observability consistency.

app/components/PhoneVerificationModal.tsx (1)

166-201: Clean up handleOtpSubmit dependencies / robustness (optional)

handleOtpSubmit’s dependency list includes onVerified and onClose, but they aren’t referenced in the function body. This is harmless but can confuse readers and hook linters.

Optionally either:

  • Remove them from the deps array, or
  • Use them (e.g. pass through success state here instead of in renderVerified).

Also, for extra safety you could guard against an unexpected empty formattedPhone before POSTing (defensive check), though the current flow already sets it on successful OTP send.

app/lib/phone-verification.ts (1)

146-150: Use a cryptographically stronger source for OTP generation

generateOTP currently uses Math.random(), which isn’t suitable for security‑sensitive codes:

export function generateOTP(): string {
  return Math.floor(100000 + Math.random() * 900000).toString();
}

For verification codes that gate KYC/limits, it’s safer to use a crypto-strength RNG, e.g.:

+import { randomInt } from 'crypto';
+
 export function generateOTP(): string {
-  return Math.floor(100000 + Math.random() * 900000).toString();
+  return randomInt(100000, 1000000).toString(); // 6 digits
 }

This keeps the UX the same while hardening against prediction.

app/components/KycModal.tsx (1)

621-625: Avoid logging potentially sensitive error details to console.

Error messages and stack traces may contain sensitive information (wallet addresses, user data) that should not be logged client-side.

     } catch (error) {
-      if (error instanceof Error) {
-        console.error("Error message:", error.message);
-        console.error("Error stack:", error.stack);
-      }
+      console.error("Failed to submit verification data");
       toast.error("Failed to submit verification data");
       setStep(STEPS.STATUS.FAILED);
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2fc36b0 and e90c5d3.

⛔ Files ignored due to path filters (1)
  • public/icons/placeholder.png is excluded by !**/*.png
📒 Files selected for processing (24)
  • .env.example (1 hunks)
  • app/api/aggregator.ts (1 hunks)
  • app/api/kyc/smile-id/route.ts (1 hunks)
  • app/api/kyc/status/route.ts (1 hunks)
  • app/api/kyc/transaction-summary/route.ts (1 hunks)
  • app/api/phone/send-otp/route.ts (1 hunks)
  • app/api/phone/status/route.ts (1 hunks)
  • app/api/phone/verify-otp/route.ts (1 hunks)
  • app/components/KycModal.tsx (14 hunks)
  • app/components/PhoneVerificationModal.tsx (1 hunks)
  • app/components/ProfileDrawer.tsx (1 hunks)
  • app/components/SettingsDropdown.tsx (4 hunks)
  • app/components/TransactionLimitModal.tsx (1 hunks)
  • app/components/index.ts (1 hunks)
  • app/hooks/useKYCStatus.ts (1 hunks)
  • app/hooks/usePhoneVerification.ts (1 hunks)
  • app/hooks/useSwapButton.ts (1 hunks)
  • app/lib/countries.ts (1 hunks)
  • app/lib/phone-verification.ts (1 hunks)
  • app/lib/smileID.ts (1 hunks)
  • app/lib/supabase.ts (2 hunks)
  • app/pages/TransactionForm.tsx (9 hunks)
  • package.json (3 hunks)
  • supabase/migrations/create_user_kyc_profiles.sql (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/PhoneVerificationModal.tsx
  • app/pages/TransactionForm.tsx
  • app/components/KycModal.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/components/PhoneVerificationModal.tsx
  • app/pages/TransactionForm.tsx
  • app/components/KycModal.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/pages/TransactionForm.tsx
🧬 Code graph analysis (14)
app/api/kyc/smile-id/route.ts (2)
app/lib/smileID.ts (1)
  • submitSmileIDJob (6-65)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/phone/status/route.ts (2)
app/api/kyc/status/route.ts (1)
  • GET (5-67)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/kyc/transaction-summary/route.ts (3)
app/api/kyc/status/route.ts (1)
  • GET (5-67)
app/api/phone/status/route.ts (1)
  • GET (5-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/hooks/useKYCStatus.ts (1)
app/api/aggregator.ts (1)
  • fetchKYCStatus (306-352)
app/components/SettingsDropdown.tsx (1)
app/components/ProfileDrawer.tsx (1)
  • ProfileDrawer (28-375)
app/components/PhoneVerificationModal.tsx (1)
app/lib/countries.ts (4)
  • Country (1-6)
  • fetchCountries (15-60)
  • searchCountries (95-104)
  • getPopularCountries (88-90)
app/api/aggregator.ts (2)
app/types.ts (1)
  • InitiateKYCResponse (231-238)
app/lib/server-analytics.ts (3)
  • trackServerEvent (130-175)
  • trackApiResponse (241-258)
  • trackBusinessEvent (362-375)
app/api/kyc/status/route.ts (3)
app/api/kyc/transaction-summary/route.ts (1)
  • GET (5-81)
app/api/phone/status/route.ts (1)
  • GET (5-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/components/ProfileDrawer.tsx (4)
app/hooks/useKYCStatus.ts (2)
  • useKYCStatus (66-187)
  • KYC_TIERS (21-46)
app/utils.ts (3)
  • classNames (23-25)
  • formatNumberWithCommas (56-60)
  • shortenAddress (536-545)
app/components/AnimatedComponents.tsx (1)
  • sidebarAnimation (117-127)
app/components/TransactionLimitModal.tsx (1)
  • TransactionLimitModal (25-221)
app/components/TransactionLimitModal.tsx (5)
app/hooks/useKYCStatus.ts (2)
  • useKYCStatus (66-187)
  • KYC_TIERS (21-46)
app/hooks/usePhoneVerification.ts (1)
  • usePhoneVerification (19-106)
app/utils.ts (1)
  • formatNumberWithCommas (56-60)
app/components/PhoneVerificationModal.tsx (1)
  • PhoneVerificationModal (28-536)
app/components/KycModal.tsx (1)
  • KycModal (57-670)
app/pages/TransactionForm.tsx (2)
app/hooks/useKYCStatus.ts (1)
  • useKYCStatus (66-187)
app/components/TransactionLimitModal.tsx (1)
  • TransactionLimitModal (25-221)
app/api/phone/verify-otp/route.ts (3)
app/api/kyc/smile-id/route.ts (1)
  • POST (5-113)
app/api/phone/send-otp/route.ts (1)
  • POST (11-100)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/components/KycModal.tsx (4)
app/components/ImageAssets.tsx (3)
  • UserDetailsIcon (677-729)
  • SadFaceIcon (771-807)
  • VerificationPendingIcon (731-770)
app/components/Styles.ts (1)
  • secondaryBtnClasses (6-6)
app/api/aggregator.ts (1)
  • submitSmileIDData (680-724)
app/hooks/analytics/useMixpanel.ts (1)
  • trackEvent (91-110)
app/api/phone/send-otp/route.ts (2)
app/lib/phone-verification.ts (4)
  • validatePhoneNumber (37-67)
  • generateOTP (149-151)
  • sendTermiiOTP (72-115)
  • sendTwilioOTP (120-144)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
🔇 Additional comments (10)
package.json (1)

36-73: New KYC/phone-related dependencies align with the rest of the PR

The additions of @smileid/web-components, smile-identity-core, libphonenumber-js, and twilio match the SmileID and phone verification flows wired up elsewhere in the PR. As long as Twilio and SmileID core are only imported from server routes (never client components), this setup looks consistent.

app/components/index.ts (1)

30-31: Re-exports look consistent with new modal components

Exporting PhoneVerificationModal and TransactionLimitModal from the central components index is consistent with how other modals are exposed and will simplify imports for callers.

app/hooks/useKYCStatus.ts (1)

66-186: KYC status hook structure looks solid

The overall design of useKYCStatus—per-wallet fetch guards, consolidated KYC + transaction summary state, and helper methods for limits and canTransact—looks coherent and appropriate for the flows consuming it. I don’t see any blocking issues here.

app/components/SettingsDropdown.tsx (1)

22-29: Profile drawer wiring from settings dropdown looks correct

The new “Profile” menu item and isProfileDrawerOpen state correctly open the ProfileDrawer while closing the dropdown, and the drawer’s onClose cleanly resets its open state. No functional issues spotted with this integration.

Also applies to: 39-40, 273-288, 314-317

app/pages/TransactionForm.tsx (1)

72-87: KYC status hook + limit modal state wiring looks consistent

Using useKYCStatus for canTransact, tier, isPhoneVerified, and refreshStatus, plus the new isLimitModalOpen / blockedTransactionAmount state, fits cleanly with the transaction-limit and KYC flows (including TransactionLimitModal). No functional issues spotted here.

app/components/KycModal.tsx (5)

8-16: LGTM: Standard web component type declaration.

The global JSX intrinsic element declaration with any type is appropriate for integrating third-party web components that lack TypeScript definitions.


200-240: LGTM: Consistent icon sizing improvements.

The icon size adjustments from size-5 to size-7 improve visual consistency, and the spacer div at line 240 maintains proper alignment for the policy link.


358-469: LGTM: Status rendering updates align with new capture flow.

The status rendering functions have been appropriately updated with new iconography (Clock05Icon), revised copy, and consistent styling that aligns with the in-app SmileID capture flow.


656-666: LGTM: Clean render pattern using object literal.

The refactored render logic using an object literal indexed by step is more concise and type-safe than a switch statement.


82-82: Remove unused isCapturing state.

The isCapturing state is declared but never set to true anywhere in the component, making the conditional rendering at lines 340-344 unreachable dead code.

-  const [isCapturing, setIsCapturing] = useState(false);

And remove the dead code:

-      {isCapturing && (
-      <div className="text-center text-sm text-gray-500">
-        Processing your verification...
-      </div>
-    )}

Also applies to: 340-344

⛔ Skipped due to learnings
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
app/lib/phone-verification.ts (1)

4-8: Guard Twilio client initialization against missing env vars.

The Twilio client is created at module load using non-null assertions. If TWILIO_ACCOUNT_SID or TWILIO_AUTH_TOKEN are absent (e.g., local dev, tests, preview environments), this will throw during module import and break any route that depends on this file, even if Twilio isn't used.

Consider lazily initializing the client inside sendTwilioOTP with explicit checks, as suggested in the previous review.

app/api/kyc/smile-id/route.ts (1)

19-24: Add server-side wallet signature verification.

The route requires walletAddress, signature, and nonce but never verifies that the signature corresponds to the wallet and message. Any caller who completes a SmileID job can associate that result with any walletAddress without proving control of the wallet.

Verify the signature server-side before trusting walletAddress or updating the KYC profile, as detailed in the previous review.

app/components/TransactionLimitModal.tsx (1)

113-124: Guard progress bar width calculation against zero monthly limit.

For tier 0, getCurrentLimits() returns { monthly: 0 }, so line 123's division produces Infinity/NaN, resulting in an invalid width: "NaN%" style. This will display a broken progress bar for unverified users.

Guard against zero monthly limit and clamp to 0-100%, as suggested in the previous review.

app/components/KycModal.tsx (1)

150-154: Add validation before proceeding to CAPTURE step.

The code proceeds to STEPS.CAPTURE without verifying that SmileID components have loaded (smileIdLoaded is true). If loading failed due to network issues, the user will encounter a broken capture screen.

Check smileIdLoaded before advancing to CAPTURE, as detailed in the previous review.

supabase/migrations/create_user_kyc_profiles.sql (1)

1-20: ⚠️ Plaintext OTP and PII storage — security & compliance risk (duplicate of past feedback).

This table stores sensitive fields in plaintext that create immediate DB-compromise and GDPR/compliance risk:

  • Line 6: phone_number TEXT NOT NULL — plaintext phone enables bulk harvesting on DB breach or backup theft.
  • Line 8: otp_code TEXT — plaintext OTP enables immediate reuse; attacker with DB access can verify/exhaust 2FA.
  • Lines 13–14: id_info JSONB, image_links JSONB — likely contain KYC documents (facial images, ID scans); plaintext JSONB = raw document exposure.

This issue was already raised in detail in past review comments (lines 4–18). Recommended fixes: hash/HMAC OTP with server KEK (KMS-managed), use deterministic or randomized field-level encryption for phone/PII, avoid plaintext JSONB for docs.

🧹 Nitpick comments (5)
app/components/KycModal.tsx (1)

641-641: Remove debug console.log statement.

Production code should not contain debug logging in event handlers.

Apply this diff:

   const handleBack = (event: any) => {
-    console.log("Back detail:", event.detail);
   };
supabase/migrations/create_user_kyc_profiles.sql (4)

28-34: Apply PL/pgSQL best practice: use CURRENT_TIMESTAMP instead of NOW().

Lines 31 and 34 use NOW(). PostgreSQL best practice is to prefer CURRENT_TIMESTAMP for consistency and to avoid function call overhead in triggers. Both are equivalent, but CURRENT_TIMESTAMP is standard SQL.

  CREATE OR REPLACE FUNCTION update_user_kyc_profiles_updated_at()
  RETURNS TRIGGER AS $$
  BEGIN
-   NEW.updated_at = NOW();
+   NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
  END;
  $$ language 'plpgsql';

43-52: Verify RLS context setup and document wallet_address injection point.

Line 48 RLS policy uses current_setting('app.wallet_address', true) to gate access. This assumes the application layer sets this context before issuing queries. If not set correctly:

  • RLS policy silently permits no rows (fails open? or closed?).
  • Test coverage may miss the misconfiguration.

Ensure the application layer (API routes, client library) explicitly calls:

SET app.wallet_address TO <wallet_address>;
-- or via Supabase client library (if available)

Add a comment or ADR documenting this dependency.


9-9: Add automatic cleanup trigger or scheduled job for expired OTPs.

Line 9 (expires_at) tracks OTP expiry, but there is no mechanism to delete expired or used OTP records. This accumulates stale data and increases DB size/query cost over time. Additionally, line 17 (attempts) tracks failed attempts but has no logic to reset or enforce limits.

Add a database trigger (on successful OTP verification) to NULL out otp_code and attempts, or add a pg_cron job to delete records where expires_at < NOW() and verified = true.

-- Example: auto-cleanup expired OTP records (run via pg_cron)
DELETE FROM user_kyc_profiles
WHERE expires_at < NOW() - INTERVAL '1 day'
  AND verified = true;

Also applies to: 17-17


4-5: Consider adding foreign key and additional constraints for data integrity.

  • Line 4 (wallet_address PRIMARY KEY): Assumes wallet address uniquely identifies a user. If a user can have multiple wallets or if there's a separate users table, add a FK to users(id) or clarify the identity model.
  • Line 12 (smile_job_id): Likely references Smile ID external API; consider adding a NOT NULL constraint once Tier 2 users complete Smile ID verification (or ensure nullable is intentional for Tier 1–only users).
  • Line 15 (tier CHECK): Good constraint; clarify in code comments that tier progression is: 0 (unverified) → 1 (phone verified) → 2 (full KYC via Smile ID).

Also applies to: 12-12, 15-15

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e90c5d3 and a9e355b.

📒 Files selected for processing (10)
  • app/api/aggregator.ts (2 hunks)
  • app/api/kyc/smile-id/route.ts (1 hunks)
  • app/api/phone/send-otp/route.ts (1 hunks)
  • app/api/phone/verify-otp/route.ts (1 hunks)
  • app/components/KycModal.tsx (14 hunks)
  • app/components/PhoneVerificationModal.tsx (1 hunks)
  • app/components/TransactionLimitModal.tsx (1 hunks)
  • app/lib/phone-verification.ts (1 hunks)
  • app/types.ts (1 hunks)
  • supabase/migrations/create_user_kyc_profiles.sql (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/api/phone/verify-otp/route.ts
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/PhoneVerificationModal.tsx
  • app/components/TransactionLimitModal.tsx
  • app/lib/phone-verification.ts
  • app/components/KycModal.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/components/PhoneVerificationModal.tsx
  • app/components/TransactionLimitModal.tsx
  • app/components/KycModal.tsx
📚 Learning: 2025-10-10T16:44:32.125Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.

Applied to files:

  • app/components/PhoneVerificationModal.tsx
  • app/components/KycModal.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/components/TransactionLimitModal.tsx
  • app/components/KycModal.tsx
📚 Learning: 2025-10-13T14:37:45.409Z
Learnt from: sundayonah
Repo: paycrest/noblocks PR: 250
File: app/components/AppLayout.tsx:33-46
Timestamp: 2025-10-13T14:37:45.409Z
Learning: In TypeScript/JavaScript/React codebases, JSX whitespace expressions like `{" "}` between elements should be classified as style/nitpick issues, not "Potential issues" or problems that affect functionality. These are purely cosmetic concerns about code cleanliness.

Applied to files:

  • app/components/KycModal.tsx
🧬 Code graph analysis (6)
app/api/aggregator.ts (2)
app/types.ts (1)
  • SmileIDSubmissionResponse (240-248)
app/lib/server-analytics.ts (3)
  • trackServerEvent (130-175)
  • trackApiResponse (241-258)
  • trackBusinessEvent (362-375)
app/components/PhoneVerificationModal.tsx (1)
app/lib/countries.ts (4)
  • Country (1-6)
  • fetchCountries (15-60)
  • searchCountries (95-104)
  • getPopularCountries (88-90)
app/components/TransactionLimitModal.tsx (5)
app/hooks/useKYCStatus.ts (2)
  • useKYCStatus (66-187)
  • KYC_TIERS (21-46)
app/hooks/usePhoneVerification.ts (1)
  • usePhoneVerification (19-106)
app/utils.ts (1)
  • formatNumberWithCommas (56-60)
app/components/PhoneVerificationModal.tsx (1)
  • PhoneVerificationModal (28-536)
app/components/KycModal.tsx (1)
  • KycModal (57-673)
app/api/kyc/smile-id/route.ts (4)
app/api/phone/send-otp/route.ts (1)
  • POST (11-103)
app/api/phone/verify-otp/route.ts (1)
  • POST (7-132)
app/lib/smileID.ts (1)
  • submitSmileIDJob (6-65)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/components/KycModal.tsx (2)
app/components/Styles.ts (1)
  • secondaryBtnClasses (6-6)
app/api/aggregator.ts (1)
  • submitSmileIDData (681-724)
app/api/phone/send-otp/route.ts (2)
app/lib/phone-verification.ts (4)
  • validatePhoneNumber (43-75)
  • generateOTP (157-159)
  • sendTermiiOTP (80-123)
  • sendTwilioOTP (128-152)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
🔇 Additional comments (7)
app/api/aggregator.ts (1)

673-724: LGTM!

The function correctly uses SmileIDSubmissionResponse as return type and avoids logging sensitive KYC payload data. Analytics tracking includes only non-PII metadata (wallet_address, jobId).

app/api/kyc/smile-id/route.ts (1)

92-99: Good addition: Row update verification.

The check for updatedProfile.length === 0 ensures that a KYC profile row was actually updated and returns 404 if not. This prevents silent failures when the phone verification step hasn't created a profile yet.

app/api/phone/send-otp/route.ts (1)

42-75: LGTM!

The code now correctly uses provider-specific phone formats: e164Format (no spaces) for Twilio and storage, digitsOnly (no + prefix) for Termii. This addresses the previous review concerns about provider compatibility.

app/components/PhoneVerificationModal.tsx (1)

114-164: LGTM!

The handlePhoneSubmit dependency array now correctly includes selectedCountry and name, ensuring the OTP is sent with the current country code and name. This addresses the previous review concern.

app/components/KycModal.tsx (1)

597-599: Good addition: Validation before submission.

The validation checks ensure that walletAddress, kycSignature, and kycNonce are present before constructing the payload. This prevents submitting invalid data to the backend.

supabase/migrations/create_user_kyc_profiles.sql (2)

27-40: Verify that update_user_kyc_profiles_updated_at function is not duplicated elsewhere in migration.

Past review comments indicate lines 27–41 and 54–66 both define the same function and trigger. The provided code shows only lines 27–40. If the duplicate exists in the full file, remove lines 54–66 to keep one function and one trigger.


23-23: Remove this review comment—it references a non-existent security requirement.

The migration file contains no security recommendation about phone_number encryption above line 23. Phone numbers are stored plaintext by design: the verification API requires equality queries on phone_number (in send-otp, verify-otp, and status endpoints), which precludes encryption. The index on phone_number is appropriate for these queryable fields. This reflects an architectural decision, not a security oversight.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/components/TransactionLimitModal.tsx (2)

55-59: Redundant undefined check for tier.

The tier === undefined check on line 56 is unnecessary because tier is typed as 0 | 1 | 2 in useKYCStatus and initialized to 0. TypeScript's type system ensures tier can never be undefined.

Apply this diff to simplify the condition:

-    useEffect(() => {
-        if (isOpen && !isLoading && (tier < 1 || tier === undefined)) {
-            setIsPhoneModalOpen(true);
-        }
-    }, [isOpen, isLoading, tier]);
+    useEffect(() => {
+        if (isOpen && !isLoading && tier < 1) {
+            setIsPhoneModalOpen(true);
+        }
+    }, [isOpen, isLoading, tier]);

67-70: Revisit: parent onClose still called twice on phone verification.

Following up on the previous discussion: when PhoneVerificationModal succeeds, it calls both onVerified(formattedPhone) and onClose() (per the component's implementation). This means:

  1. onVerified fires → handlePhoneVerified runs → calls onClose() (closes parent)
  2. onClose fires → the prop at lines 202-205 runs → calls onClose() (closes parent again)

While calling setIsPhoneModalOpen(false) twice is harmless, invoking the parent onClose() twice could cause unintended side effects depending on what that callback does elsewhere in the app.

To eliminate the redundant call while preserving explicit control over the phone modal state, consider:

 const handlePhoneVerified = async (phoneNumber: string) => {
+    await Promise.all([refreshStatus(), refreshPhoneStatus()]);
     setIsPhoneModalOpen(false);
-    onClose();
+    // onClose will be called by PhoneVerificationModal's onClose prop
 };

This way, handlePhoneVerified refreshes state and closes the phone modal, while the PhoneVerificationModal's onClose prop (lines 202–205) handles closing the parent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9e355b and 1069784.

📒 Files selected for processing (2)
  • .env.example (1 hunks)
  • app/components/TransactionLimitModal.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/TransactionLimitModal.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/components/TransactionLimitModal.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/components/TransactionLimitModal.tsx
🧬 Code graph analysis (1)
app/components/TransactionLimitModal.tsx (5)
app/hooks/useKYCStatus.ts (2)
  • useKYCStatus (66-187)
  • KYC_TIERS (21-46)
app/hooks/usePhoneVerification.ts (1)
  • usePhoneVerification (19-106)
app/utils.ts (1)
  • formatNumberWithCommas (56-60)
app/components/PhoneVerificationModal.tsx (1)
  • PhoneVerificationModal (28-536)
app/components/KycModal.tsx (1)
  • KycModal (57-673)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 113-113: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 114-114: [UnorderedKey] The SMILE_IDENTITY_API_KEY key should go before the SMILE_IDENTITY_BASE_URL key

(UnorderedKey)


[warning] 116-116: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 116-116: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 117-117: [UnorderedKey] The SMILE_IDENTITY_SERVER key should go before the SMILE_ID_CALLBACK_URL key

(UnorderedKey)


[warning] 117-117: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🔇 Additional comments (2)
app/components/TransactionLimitModal.tsx (2)

113-132: Progress bar guard implemented correctly.

The division-by-zero guard for tier 0 users (when currentLimits.monthly is 0) has been properly implemented. The progress bar now safely displays "0%" instead of "NaN%" when the monthly limit is zero, and correctly clamps the percentage to a maximum of 100%.


209-221: Verify: Missing state refresh and user tracking after KYC completion.

Two potential issues with the KYC modal integration:

  1. Empty callback: setIsUserVerified={() => { }} (line 217) passes an empty function to KycModal. If the parent component should track when KYC verification succeeds (to potentially refresh limits or update UI), this callback may need implementation.

  2. No status refresh after KYC: Unlike handlePhoneVerified (which could call refreshStatus()), there's no explicit refresh of KYC status after the KYC modal closes successfully. While KycModal may handle internal state updates, the TransactionLimitModal won't reflect the new tier/limits until the user reopens it or the page refreshes.

Consider:

                     <AnimatedModal
                         isOpen={isKycModalOpen}
-                        onClose={() => { setIsKycModalOpen(false); onClose(); }}
+                        onClose={async () => {
+                            setIsKycModalOpen(false);
+                            await refreshStatus(); // Refresh to reflect new tier
+                            onClose();
+                        }}
                     >
                         <KycModal
                             setIsKycModalOpen={setIsKycModalOpen}
-                            setIsUserVerified={() => { }}
+                            setIsUserVerified={async (verified) => {
+                                if (verified) {
+                                    await refreshStatus();
+                                }
+                            }}
                         />
                     </AnimatedModal>

Verify whether KycModal already triggers refreshStatus via a different mechanism, or if this refresh should be added here.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
app/api/phone/verify-otp/route.ts (3)

62-76: Add error tracking for observability.

The expiration and max-attempts checks return error responses but don't call trackApiError, unlike other error paths in this handler (lines 18, 28, 44, 88, 117, 136). This reduces observability.

Apply this diff to add consistent error tracking:

     // Check expiration
     if (new Date() > new Date(verification.expires_at)) {
+      trackApiError(request, '/api/phone/verify-otp', 'POST', new Error('OTP expired'), 400);
       return NextResponse.json(
         { success: false, error: 'OTP has expired. Please request a new one.' },
         { status: 400 }
       );
     }
 
     // Check attempts
     if (verification.attempts >= MAX_ATTEMPTS) {
+      trackApiError(request, '/api/phone/verify-otp', 'POST', new Error('Max attempts exceeded'), 429);
       return NextResponse.json(
         { success: false, error: 'Maximum verification attempts exceeded. Please request a new OTP.' },
         { status: 429 }
       );
     }

105-113: Minor: Fix whitespace formatting.

Line 110 has inconsistent spacing (space before the comma).

Apply this diff:

       .update({ 
         verified: true, 
-        verified_at: new Date().toISOString() ,
+        verified_at: new Date().toISOString(),
         tier: 1
       })

127-132: Consider returning the normalized phone number for consistency.

The response returns the original phoneNumber from the request body rather than the normalized E.164 format stored in the database. While this may be intentional for UX, it could lead to inconsistency if other endpoints return the normalized format.

Optionally, return the normalized format for consistency:

     return NextResponse.json({
       success: true,
       message: 'Phone number verified successfully',
       verified: true,
-      phoneNumber: phoneNumber
+      phoneNumber: validation.e164Format
     });

Or use property shorthand if keeping the original:

-      phoneNumber: phoneNumber
+      phoneNumber
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1069784 and 8beb003.

📒 Files selected for processing (1)
  • app/api/phone/verify-otp/route.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/api/phone/verify-otp/route.ts (3)
app/api/phone/send-otp/route.ts (1)
  • POST (11-103)
app/lib/phone-verification.ts (1)
  • validatePhoneNumber (43-75)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
🔇 Additional comments (5)
app/api/phone/verify-otp/route.ts (5)

1-6: LGTM!

Imports and constants are appropriate for the OTP verification flow.


17-33: LGTM!

Input validation is thorough and properly normalizes phone numbers to E.164 format for consistent querying.


35-49: LGTM!

Verification record lookup correctly uses normalized E.164 format and lowercased wallet address for consistent querying.


51-60: LGTM!

Idempotent check for already-verified records is appropriate and prevents unnecessary processing.


134-142: LGTM!

Catch-all error handling appropriately logs, tracks, and returns a generic 500 response.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
app/components/KycModal.tsx (3)

489-493: Remove commented-out code.

The commented lines at 490-491 are from the old QR-based flow and are no longer needed.

Apply this diff:

       if (newStatus === STEPS.STATUS.PENDING) {
-        // setKycUrl(response.data.url);
-        // setIsKycModalOpen(true);
         return;
       }

628-630: Remove debug console.log statement.

The console.log at Line 629 should be removed for production code.

Apply this diff:

     const handleBack = (event: any) => {
-      console.log("Back detail:", event.detail);
     };

145-149: Add validation before proceeding to CAPTURE step.

The code proceeds to the CAPTURE step without verifying that SmileID components have successfully loaded. If smileIdLoaded is false (e.g., due to network issues or loading failure), the user will encounter a broken capture screen.

Apply this diff to add a guard:

        setKycSignature(sigWithoutPrefix);
        setKycNonce(nonce);

-        // Skip old KYC initiation since we're using Smile ID
-        setStep(STEPS.CAPTURE);
+        // Check if SmileID components are loaded before proceeding
+        if (smileIdLoaded) {
+          setStep(STEPS.CAPTURE);
+        } else {
+          toast.error("Verification component not ready. Please try again.");
+          setIsKycModalOpen(false);
+          setStep(STEPS.TERMS);
+        }
🧹 Nitpick comments (5)
app/components/TransactionLimitModal.tsx (2)

59-62: Refresh KYC status after phone verification.

After successful phone verification, the KYC status (tier, limits, etc.) should be refreshed so the user sees their updated tier when this modal reopens. Currently, the status remains stale until the next time the modal is opened.

Apply this diff to refresh status after verification:

 const handlePhoneVerified = async (phoneNumber: string) => {
+    await refreshStatus();
     setIsPhoneModalOpen(false);
     onClose();
 };

207-210: Consider handling KYC completion state.

The setIsUserVerified prop is passed as an empty function, meaning successful Tier 2 KYC completion won't update any state in this component. If the parent component needs to know about KYC completion, consider passing a real handler or refreshing status when KycModal closes.

If status tracking is needed:

 <KycModal
     setIsKycModalOpen={setIsKycModalOpen}
-    setIsUserVerified={() => { }}
+    setIsUserVerified={async () => {
+        await refreshStatus();
+    }}
 />
app/components/KycModal.tsx (1)

83-95: Remove debug console.log statement.

The console.log at Line 87 should be removed for production code.

Apply this diff:

     import("@smileid/web-components/smart-camera-web")
       .then(() => {
-        console.log("SmileID web components loaded");
         setSmileIdLoaded(true);
       })
app/api/phone/verify-otp/route.ts (2)

62-76: Add analytics logging for OTP expiry/limit/invalid flows

The business rules for Line 62–76 and Line 78–114 look right, but these key 4xx outcomes aren’t currently logged via trackApiError or trackApiResponse, which makes it harder to monitor OTP failures vs system errors.

Consider adding explicit error tracking in these branches, e.g.:

     // Check expiration
     if (new Date() > new Date(verification.expires_at)) {
+      trackApiError(request, '/api/phone/verify-otp', 'POST', new Error('OTP expired'), 400);
       return NextResponse.json(
         { success: false, error: 'OTP has expired. Please request a new one.' },
         { status: 400 }
       );
     }

     // Check attempts
     if (verification.attempts >= MAX_ATTEMPTS) {
+      trackApiError(request, '/api/phone/verify-otp', 'POST', new Error('Maximum verification attempts exceeded'), 429);
       return NextResponse.json(
         { success: false, error: 'Maximum verification attempts exceeded. Please request a new OTP.' },
         { status: 429 }
       );
     }

     // Verify OTP
     if (verification.otp_code !== otpCode) {
       // Atomic increment with boundary check to prevent race conditions
       const { data: updated, error: attemptsError } = await supabaseAdmin
         .from('user_kyc_profiles')
         .update({ attempts: verification.attempts + 1 })
         .eq('wallet_address', walletAddress.toLowerCase())
         .lt('attempts', MAX_ATTEMPTS)
         .select('attempts')
         .single();

       if (attemptsError) {
         console.error('Failed to increment OTP attempts:', attemptsError);
         trackApiError(request, '/api/phone/verify-otp', 'POST', attemptsError, 500);
         return NextResponse.json(
           { success: false, error: 'Failed to process verification attempt' },
           { status: 500 }
         );
       }

       // If no rows updated, attempts limit was hit mid-flight (race condition)
       if (!updated) {
+        trackApiError(request, '/api/phone/verify-otp', 'POST', new Error('Maximum verification attempts exceeded'), 429);
         return NextResponse.json(
           { success: false, error: 'Maximum verification attempts exceeded. Please request a new OTP.' },
           { status: 429 }
         );
       }

+      trackApiError(request, '/api/phone/verify-otp', 'POST', new Error('Invalid OTP code'), 400);
       return NextResponse.json(
         {
           success: false,
           error: 'Invalid OTP code',
           attemptsRemaining: MAX_ATTEMPTS - updated.attempts
         },
         { status: 400 }
       );
     }

This keeps analytics for verification failures on par with the /send-otp route.

Also applies to: 78-114


116-143: Avoid unintentionally downgrading existing KYC tier when marking phone verified

On Line 119–123 you always set tier: 1 when the OTP is correct. If this route is ever hit for a profile that already has a higher tier (e.g., after SmileID full KYC), this would silently down-tier them.

If higher tiers are possible on user_kyc_profiles, consider preserving the maximum tier instead of forcing 1, for example by only updating when the current tier is lower, or using a DB-side GREATEST:

-    const { error: updateError } = await supabaseAdmin
-      .from('user_kyc_profiles')
-      .update({ 
-        verified: true, 
-        verified_at: new Date().toISOString() ,
-        tier: 1
-      })
-      .eq('wallet_address', walletAddress.toLowerCase());
+    const { error: updateError } = await supabaseAdmin
+      .from('user_kyc_profiles')
+      .update({
+        verified: true,
+        verified_at: new Date().toISOString(),
+        // e.g. leave tier unchanged if already >= 1
+        tier: 1,
+      })
+      .eq('wallet_address', walletAddress.toLowerCase());
+      // Optionally add a condition or DB-side expression to avoid lowering a higher tier
+      // .lte('tier', 1)

Adjust the exact logic to match how tiers are modeled elsewhere in the KYC flow.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8beb003 and bc48350.

📒 Files selected for processing (3)
  • app/api/phone/verify-otp/route.ts (1 hunks)
  • app/components/KycModal.tsx (14 hunks)
  • app/components/TransactionLimitModal.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/TransactionLimitModal.tsx
  • app/components/KycModal.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/components/TransactionLimitModal.tsx
  • app/components/KycModal.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/components/TransactionLimitModal.tsx
  • app/components/KycModal.tsx
📚 Learning: 2025-10-13T14:37:45.409Z
Learnt from: sundayonah
Repo: paycrest/noblocks PR: 250
File: app/components/AppLayout.tsx:33-46
Timestamp: 2025-10-13T14:37:45.409Z
Learning: In TypeScript/JavaScript/React codebases, JSX whitespace expressions like `{" "}` between elements should be classified as style/nitpick issues, not "Potential issues" or problems that affect functionality. These are purely cosmetic concerns about code cleanliness.

Applied to files:

  • app/components/KycModal.tsx
🧬 Code graph analysis (3)
app/components/TransactionLimitModal.tsx (4)
app/hooks/useKYCStatus.ts (2)
  • useKYCStatus (66-187)
  • KYC_TIERS (21-46)
app/utils.ts (1)
  • formatNumberWithCommas (56-60)
app/components/PhoneVerificationModal.tsx (1)
  • PhoneVerificationModal (28-536)
app/components/KycModal.tsx (1)
  • KycModal (55-661)
app/api/phone/verify-otp/route.ts (3)
app/api/phone/send-otp/route.ts (1)
  • POST (11-103)
app/lib/phone-verification.ts (1)
  • validatePhoneNumber (43-75)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/components/KycModal.tsx (4)
app/components/ImageAssets.tsx (3)
  • UserDetailsIcon (677-729)
  • SadFaceIcon (771-807)
  • VerificationPendingIcon (731-770)
app/components/Styles.ts (1)
  • secondaryBtnClasses (6-6)
app/api/aggregator.ts (1)
  • submitSmileIDData (681-724)
app/hooks/analytics/useMixpanel.ts (1)
  • trackEvent (91-110)
🔇 Additional comments (1)
app/api/phone/verify-otp/route.ts (1)

25-41: Phone normalization and lookup logic looks solid

Using validatePhoneNumber plus querying by wallet_address.toLowerCase() and normalized validation.e164Format keeps /send-otp and /verify-otp in sync and avoids subtle mismatch bugs between display formats and stored values.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
app/api/phone/verify-otp/route.ts (2)

60-69: Consider returning phoneNumber in already-verified response for consistency.

The already-verified response doesn't include the phoneNumber field, while the successful verification response at line 151 does. For consistency and client convenience, consider adding it:

     return NextResponse.json({
       success: true,
       message: 'Phone number already verified',
-      verified: true
+      verified: true,
+      phoneNumber: validation.internationalFormat
     });

147-152: Return normalized phone number format for consistency.

Line 151 echoes back the raw phoneNumber from the request body. For consistency with the send-otp endpoint (which returns validation.internationalFormat) and to confirm the actual verified format, return the normalized value:

     return NextResponse.json({
       success: true,
       message: 'Phone number verified successfully',
       verified: true,
-      phoneNumber: phoneNumber
+      phoneNumber: validation.internationalFormat
     });
app/api/kyc/status/route.ts (1)

36-45: Consider more explicit nullish check for tier fallback.

The condition !kycProfile?.tier will be true for both undefined and 0, meaning an explicitly stored tier: 0 would trigger recalculation. While tier 0 appears unused in the current system, a more explicit check would be clearer:

-    if (!kycProfile?.tier) {
+    if (kycProfile?.tier === undefined || kycProfile?.tier === null) {
       if (fullKYCVerified) {
app/components/KycModal.tsx (2)

145-149: Add guard to ensure SmileID components loaded before advancing to CAPTURE.

While the dynamic import typically completes quickly, proceeding to CAPTURE before smileIdLoaded is true could result in a blank screen on slow connections. Consider:

        setKycSignature(sigWithoutPrefix);
        setKycNonce(nonce);

-        // Skip old KYC initiation since we're using Smile ID
-        setStep(STEPS.CAPTURE);
+        // Ensure SmileID components are loaded before proceeding
+        if (smileIdLoaded) {
+          setStep(STEPS.CAPTURE);
+        } else {
+          toast.error("Verification component not ready. Please try again.");
+          setIsKycModalOpen(false);
+          setStep(STEPS.TERMS);
+        }

Alternatively, disable the "Accept and sign" button at line 300 until smileIdLoaded is true:

          disabled={!termsAccepted || isSigning}
+          disabled={!termsAccepted || isSigning || !smileIdLoaded}

489-493: Consider removing commented-out code.

The commented lines from the old QR-based flow are no longer needed and can be removed for cleaner code:

      if (newStatus === STEPS.STATUS.PENDING) {
-        // setKycUrl(response.data.url);
-        // setIsKycModalOpen(true);
        return;
      }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc48350 and ee6d6ff.

📒 Files selected for processing (7)
  • app/api/aggregator.ts (2 hunks)
  • app/api/kyc/smile-id/route.ts (1 hunks)
  • app/api/kyc/status/route.ts (1 hunks)
  • app/api/phone/verify-otp/route.ts (1 hunks)
  • app/components/KycModal.tsx (15 hunks)
  • app/hooks/useKYCStatus.ts (1 hunks)
  • middleware.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/KycModal.tsx
  • app/api/phone/verify-otp/route.ts
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/components/KycModal.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/components/KycModal.tsx
📚 Learning: 2025-10-13T14:37:45.409Z
Learnt from: sundayonah
Repo: paycrest/noblocks PR: 250
File: app/components/AppLayout.tsx:33-46
Timestamp: 2025-10-13T14:37:45.409Z
Learning: In TypeScript/JavaScript/React codebases, JSX whitespace expressions like `{" "}` between elements should be classified as style/nitpick issues, not "Potential issues" or problems that affect functionality. These are purely cosmetic concerns about code cleanliness.

Applied to files:

  • app/components/KycModal.tsx
🧬 Code graph analysis (6)
app/api/kyc/status/route.ts (2)
app/api/phone/status/route.ts (1)
  • GET (5-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/aggregator.ts (2)
app/types.ts (1)
  • SmileIDSubmissionResponse (240-248)
app/lib/server-analytics.ts (3)
  • trackServerEvent (130-175)
  • trackApiResponse (241-258)
  • trackBusinessEvent (362-375)
app/api/kyc/smile-id/route.ts (4)
app/api/phone/verify-otp/route.ts (1)
  • POST (8-163)
app/api/phone/send-otp/route.ts (1)
  • POST (11-103)
app/lib/smileID.ts (1)
  • submitSmileIDJob (6-65)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/hooks/useKYCStatus.ts (1)
app/api/aggregator.ts (1)
  • fetchKYCStatus (307-353)
app/components/KycModal.tsx (3)
app/components/Styles.ts (1)
  • secondaryBtnClasses (6-6)
app/api/aggregator.ts (1)
  • submitSmileIDData (682-729)
app/hooks/analytics/useMixpanel.ts (1)
  • trackEvent (91-110)
app/api/phone/verify-otp/route.ts (3)
app/api/phone/send-otp/route.ts (1)
  • POST (11-103)
app/lib/phone-verification.ts (1)
  • validatePhoneNumber (43-75)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
🔇 Additional comments (5)
middleware.ts (1)

267-268: LGTM! KYC routes are now properly protected.

Adding these routes to the middleware matcher ensures JWT authentication and wallet address verification before accessing KYC endpoints, addressing the security concerns raised in previous review discussions.

app/api/aggregator.ts (1)

673-729: LGTM! Function follows established patterns.

The implementation correctly:

  • Uses JWT authentication via Bearer token
  • Tracks analytics without exposing PII
  • Returns the properly typed SmileIDSubmissionResponse
  • Follows error handling patterns consistent with other functions in this file
app/api/kyc/smile-id/route.ts (1)

5-132: LGTM! Security and data integrity concerns properly addressed.

The route implementation is solid:

  • ✅ Protected by middleware JWT authentication (resolves previous security concerns)
  • ✅ Validates all required fields before submission
  • ✅ Explicitly checks that DB row was updated (lines 102-108), ensuring profile exists
  • ✅ Comprehensive error handling for SmileID job states

The signature/nonce stored in the body are audit trail data; authentication is correctly handled via Privy JWT in middleware.

app/hooks/useKYCStatus.ts (1)

66-199: LGTM! Hook implementation is well-structured.

The hook correctly:

  • Implements per-wallet fetch guards to prevent duplicate concurrent requests
  • Uses JWT authentication for secure API calls
  • Derives tier and limits from KYC status and transaction data
  • Properly chains useCallback dependencies to trigger refetch on wallet changes
app/components/KycModal.tsx (1)

563-645: LGTM! SmileID event handling is well-implemented.

The event handling correctly:

  • Guards execution to only run when in CAPTURE step with camera element ready
  • Validates all required data before submission (addresses previous validation concerns)
  • Uses JWT authentication for secure API calls
  • Provides comprehensive error handling and user feedback
  • Properly cleans up event listeners on unmount

…rate limiting

- Added rate limiting to SmileID KYC submission endpoint.
- Updated KYC submission to include id_info for biometric verification.
- Enhanced error handling for SmileID job completion and success checks.
- Modified KYC status retrieval to reflect changes in verification tiers.
- Removed deprecated phone verification status endpoint.
- Refactored KYC modal to support ID document selection and capture.
- Updated database schema for user KYC profiles to include new fields and indexes.
- Removed unused phone verification hook.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/components/KycModal.tsx (2)

166-182: Align KYC terms copy with actual data stored (PII/compliance risk).

The bullets state:

  • “We do not store any personal information. All personal data is handled exclusively by our third-party KYC provider.”
  • “We only store the KYC reference code and signing wallet address for verification and audit purposes.”

However, the SmileID flow persists additional PII in user_kyc_profiles (e.g. id_type, id_number, id_country, full_name, date_of_birth, email_address, platform entries). This makes the current copy misleading from a privacy/compliance standpoint.

Either:

  • update the text to accurately describe what is stored and for how long, or
  • change the backend to only store what the text promises (which seems unlikely given KYC requirements).

Right now, the mismatch should be treated as a compliance‑level issue.


570-667: Fix pending-status polling effect to depend on step (currently uses stale value).

The polling useEffect only depends on walletAddress but reads step. Since step is omitted from the dependency array (and lint is disabled), the effect captures whatever step was on first run (typically LOADING) and will never start polling when step is later set to PENDING.

To make polling work as intended:

-  useEffect(() => {
+  useEffect(() => {
+    if (!walletAddress) return;
+
     let timeoutId: NodeJS.Timeout;
     const startTime = Date.now();
@@
-    debouncedFetchStatus(); // Initial fetch
+    debouncedFetchStatus(); // Initial fetch
@@
-    if (step === STEPS.STATUS.PENDING) {
+    if (step === STEPS.STATUS.PENDING) {
       intervalId = setInterval(() => {
@@
-    return () => {
+    return () => {
@@
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [walletAddress]);
+  }, [walletAddress, step]);

This way, when step transitions to PENDING, the effect re-runs, sets up the interval, and stops polling once the status changes or times out.

♻️ Duplicate comments (3)
app/components/KycModal.tsx (2)

594-598: Remove reintroduced commented-out QR flow remnants.

The newStatus === STEPS.STATUS.PENDING branch still contains commented-out code for the old QR-based flow:

// setKycUrl(response.data.url);
// setIsKycModalOpen(true);

Since this flow has been superseded by the embedded SmileID flow, these comments are now dead code and should be removed to keep the status handler clean.

-      if (newStatus === STEPS.STATUS.PENDING) {
-        // setKycUrl(response.data.url);
-        // setIsKycModalOpen(true);
-        return;
-      }
+      if (newStatus === STEPS.STATUS.PENDING) {
+        return;
+      }

386-401: Gate transition to CAPTURE on SmileID load state to avoid a broken capture screen.

The “Continue” button in renderIdInfo moves straight to STEPS.CAPTURE without checking smileIdLoaded. If the dynamic import failed (or is still in flight), the user will see a non-functional <smart-camera-web> screen with no clear recovery.

Reuse the existing smileIdLoaded flag here, e.g.:

-        <button
-          type="button"
-          className={`${primaryBtnClasses} w-full`}
-          disabled={!selectedCountry || !selectedIdType || (!needsDocCapture && !idNumber)}
-          onClick={() => setStep(STEPS.CAPTURE)}
-        >
+        <button
+          type="button"
+          className={`${primaryBtnClasses} w-full`}
+          disabled={
+            !selectedCountry ||
+            !selectedIdType ||
+            (!needsDocCapture && !idNumber) ||
+            !smileIdLoaded
+          }
+          onClick={() => {
+            if (!smileIdLoaded) {
+              toast.error("Verification component not ready. Please try again.");
+              return;
+            }
+            setStep(STEPS.CAPTURE);
+          }}
+        >

This prevents advancing into CAPTURE when the SmileID component failed to initialize.

Also applies to: 406-437

app/lib/smileID.ts (1)

76-84: Avoid logging full SmileID error payloads to prevent KYC/PII leakage.

console.error currently logs error.response?.data wholesale, which can include sensitive KYC metadata (IDs, tokens, URLs). This is risky for logs that may be long‑lived or sent to third‑party sinks.

Recommend logging only high‑level fields and optional sanitized codes, not the raw data object, e.g.:

-  } catch (error: any) {
-    console.error('SmileID API Error:', {
-      status: error.response?.status,
-      statusText: error.response?.statusText,
-      data: error.response?.data,
-      message: error.message,
-    });
-    throw error;
-  }
+  } catch (error: any) {
+    console.error("SmileID API Error:", {
+      status: error.response?.status,
+      statusText: error.response?.statusText,
+      message: error.message,
+      // Optionally surface a non-PII error code if present
+      code: error.response?.data?.code,
+    });
+    throw error;
+  }

If you need richer diagnostics, explicitly pick/whitelist non‑sensitive fields rather than dumping the full response.

🧹 Nitpick comments (3)
app/components/PhoneVerificationModal.tsx (1)

166-201: Remove unused dependencies from handleOtpSubmit.

The onVerified and onClose dependencies are listed in the dependency array but are never called within the function body. The function only sets the step to VERIFIED; those callbacks are invoked later in renderVerified (lines 504-505).

Including unused dependencies causes unnecessary re-creation of the callback and violates React useCallback optimization best practices.

Apply this diff:

-  }, [otpCode, formattedPhone, walletAddress, onVerified, onClose]);
+  }, [otpCode, formattedPhone, walletAddress]);
app/api/kyc/smile-id/route.ts (1)

38-45: Add server-side validation for ID number on Enhanced KYC ID types.

The frontend enforces ID number input for certain ID types, but this route only checks country and id_type. For Enhanced KYC types (e.g. BVN/NIN variants that map to job_type 5 in getJobTypeForIdType), id_info.id_number is effectively required; without it, submitSmileIDJob will likely fail and you’ll surface a 500 instead of a clear 4xx.

Consider mirroring the job-type logic here and validating ID number for those types before calling SmileID, e.g.:

import { getJobTypeForIdType } from '@/app/lib/smileID';

// ...
if (!id_info?.country || !id_info?.id_type) {
  return NextResponse.json(
    { status: 'error', message: 'Missing id_info: country and id_type are required' },
    { status: 400 },
  );
}

const jobType = getJobTypeForIdType(id_info.id_type);
if (jobType === 5 && !id_info.id_number) {
  return NextResponse.json(
    { status: 'error', message: 'ID number is required for this ID type' },
    { status: 400 },
  );
}

This keeps validation consistent between FE and BE and avoids unnecessary SmileID job failures.

Also applies to: 70-81, 120-127

app/components/KycModal.tsx (1)

118-128: Remove debug console.log from SmileID component loader.

The dynamic import logs "SmileID web components loaded" on every successful load. This is useful during development but noisy in production.

You can safely drop this log and keep the error path:

-      import("@smileid/web-components/smart-camera-web")
-        .then(() => {
-          console.log("SmileID web components loaded");
-          setSmileIdLoaded(true);
-        })
+      import("@smileid/web-components/smart-camera-web")
+        .then(() => {
+          setSmileIdLoaded(true);
+        })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee6d6ff and 651af93.

📒 Files selected for processing (6)
  • app/api/kyc/smile-id/route.ts (1 hunks)
  • app/api/kyc/status/route.ts (1 hunks)
  • app/api/phone/send-otp/route.ts (1 hunks)
  • app/components/KycModal.tsx (14 hunks)
  • app/components/PhoneVerificationModal.tsx (1 hunks)
  • app/lib/smileID.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/KycModal.tsx
  • app/components/PhoneVerificationModal.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/components/KycModal.tsx
  • app/components/PhoneVerificationModal.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/components/KycModal.tsx
  • app/components/PhoneVerificationModal.tsx
📚 Learning: 2025-10-13T14:37:45.409Z
Learnt from: sundayonah
Repo: paycrest/noblocks PR: 250
File: app/components/AppLayout.tsx:33-46
Timestamp: 2025-10-13T14:37:45.409Z
Learning: In TypeScript/JavaScript/React codebases, JSX whitespace expressions like `{" "}` between elements should be classified as style/nitpick issues, not "Potential issues" or problems that affect functionality. These are purely cosmetic concerns about code cleanliness.

Applied to files:

  • app/components/KycModal.tsx
🧬 Code graph analysis (3)
app/api/phone/send-otp/route.ts (3)
app/lib/rate-limit.ts (1)
  • rateLimit (10-30)
app/lib/phone-verification.ts (4)
  • validatePhoneNumber (43-75)
  • generateOTP (157-159)
  • sendTermiiOTP (80-123)
  • sendTwilioOTP (128-152)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/kyc/smile-id/route.ts (3)
app/lib/rate-limit.ts (1)
  • rateLimit (10-30)
app/lib/smileID.ts (2)
  • submitSmileIDJob (22-85)
  • SmileIDIdInfo (5-12)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/kyc/status/route.ts (2)
app/api/kyc/transaction-summary/route.ts (1)
  • GET (5-81)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
🔇 Additional comments (2)
app/api/phone/send-otp/route.ts (1)

1-111: LGTM! Clean implementation of OTP sending with proper phone normalization.

The implementation correctly addresses the previous review feedback:

  • Uses e164Format (no spaces) for database storage and Twilio
  • Uses digitsOnly (no + prefix) for Termii
  • Returns internationalFormat (with spaces) for client display

Error handling, rate limiting, and analytics tracking are all properly implemented.

app/components/PhoneVerificationModal.tsx (1)

1-538: Well-implemented phone verification flow with clean state management.

The component properly handles the three-step verification process with:

  • Comprehensive country selection with search and popular countries prioritization
  • Robust OTP input with auto-focus and backspace navigation
  • Proper state reset on close and after verification
  • Error handling with user-friendly toast notifications

The previous dependency issue in handlePhoneSubmit has been correctly addressed.

Comment on lines 22 to 45
// Check KYC profile for phone and SmileID verification status
const { data: kycProfile } = await supabaseAdmin
.from('user_kyc_profiles')
.select('verified, phone_number, tier')
.eq('wallet_address', walletAddress.toLowerCase())
.single();

const tier: 0 | 1 | 2 = (kycProfile?.tier as 0 | 1 | 2) || 0;
const phoneNumber = kycProfile?.phone_number || null;
const phoneVerified = kycProfile?.verified || false;

// Full KYC (SmileID) is verified if tier is 2
const fullKYCVerified = tier >= 2;

const responseTime = Date.now() - startTime;
trackApiResponse( '/api/kyc/status', 'GET', 200, responseTime);

return NextResponse.json({
success: true,
tier,
isPhoneVerified: phoneVerified,
phoneNumber,
isFullyVerified: fullKYCVerified,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle Supabase errors instead of silently treating KYC as “unverified”.

The supabaseAdmin.from('user_kyc_profiles')...single() call ignores error, and the code always falls back to tier = 0 and success: true. A DB or query failure will be exposed to clients as “wallet not verified” rather than as a server error, and no analytics are recorded.

Consider explicitly handling errors and tracking them, while still treating the “no row” case as tier 0. For example:

-    const { data: kycProfile } = await supabaseAdmin
-      .from('user_kyc_profiles')
-      .select('verified, phone_number, tier')
-      .eq('wallet_address', walletAddress.toLowerCase())
-      .single();
+    const { data: kycProfile, error } = await supabaseAdmin
+      .from('user_kyc_profiles')
+      .select('verified, phone_number, tier')
+      .eq('wallet_address', walletAddress)
+      .maybeSingle();
+
+    if (error) {
+      console.error('Error fetching KYC profile:', error);
+      trackApiError(request, '/api/kyc/status', 'GET', error, 500);
+      return NextResponse.json(
+        { success: false, error: 'Failed to fetch KYC status' },
+        { status: 500 },
+      );
+    }

This keeps the “no profile yet” case as kycProfile === null (tier 0) but treats real DB errors as 500s and logs them properly.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/api/kyc/status/route.ts around lines 22 to 45, the Supabase query result
is used without checking for error so any DB/query failure is treated as “no
profile” and returns success: true; change the call to capture both data and
error, if error is present (and not just “no rows”) log/track the error and
return a 500 response; keep the existing behavior that data === null means tier
0 and proceed to return success, but ensure real DB errors are handled
explicitly (trackApiResponse with 500 and include error details in
logs/analytics before returning the error response).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
supabase/migrations/create_user_kyc_profiles.sql (1)

38-45: Index strategy is well-aligned with expected query patterns.

Indexes cover the primary lookups: tier (verification status), id_number (KYC lookups), user_id (account association), phone_number (phone verification), email_address, verified status, and the GIN index on platform (JSONB array of verification platforms) is appropriate.

Note: The plaintext phone_number index (line 43) was flagged in a prior review as a potential security/privacy concern. This was explicitly declined; no change needed unless risk appetite changes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 651af93 and 3692388.

📒 Files selected for processing (1)
  • supabase/migrations/create_user_kyc_profiles.sql (1 hunks)
🔇 Additional comments (1)
supabase/migrations/create_user_kyc_profiles.sql (1)

3-36: Schema design is appropriate for progressive KYC collection.

The table structure supports the tiered KYC flow with nullable fields for optional/progressive data collection, sensible defaults (tier=0, verified=false), and sound constraints on provider and tier values.

Comment on lines +47 to +49
create trigger update_user_kyc_profiles_updated_at BEFORE
update on user_kyc_profiles for EACH row
execute FUNCTION update_user_kyc_profiles_updated_at (); No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the function definition in the migration file
find . -name "create_user_kyc_profiles.sql" -type f -exec cat {} \;

Repository: paycrest/noblocks

Length of output: 2386


🏁 Script executed:

#!/bin/bash
# Search for the function definition across all migration files and code
rg "create or replace function update_user_kyc_profiles_updated_at|create function update_user_kyc_profiles_updated_at" --type sql

Repository: paycrest/noblocks

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Also check for any variant function names
rg "update_user_kyc_profiles_updated_at" --type sql

Repository: paycrest/noblocks

Length of output: 273


The trigger references a function that does not exist in the codebase.

The trigger update_user_kyc_profiles_updated_at calls a function update_user_kyc_profiles_updated_at() that is not defined anywhere. This migration will fail with "function update_user_kyc_profiles_updated_at() does not exist" when executed.

Define the function before the trigger or verify it exists in a separate migration that runs first.

🤖 Prompt for AI Agents
In supabase/migrations/create_user_kyc_profiles.sql around lines 47-49, the
trigger creation references a function update_user_kyc_profiles_updated_at()
that does not exist; add or ensure a prior migration defines a trigger function
with that exact name and a trigger signature returning trigger (for example a
function that sets NEW.updated_at = now() and returns NEW), or move the CREATE
FUNCTION statement above this CREATE TRIGGER so the trigger can reference the
existing function; ensure names and schema match and migration ordering
guarantees the function exists before the trigger is created.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/pages/TransactionForm.tsx (1)

72-87: Fix critical currency mismatch and type safety in transaction limit check

In handleSwap, the transaction limit check has two critical issues:

  1. Currency unit mismatchamountReceived is in the selected payout currency (e.g., GHS, NGN), but transactionSummary.monthlySpent from the backend is accumulated from amount_sent values in the source currency (typically USD). Passing amountReceived directly to canTransact() compares incompatible units and breaks the limit guard. You must convert amountReceived to the same currency as monthlySpent before checking limits.

  2. Type safetygetValues() can return strings; explicitly coerce to number:

-    const formData = getValues();
-    const usdAmount = formData.amountReceived || 0;
+    const { amountReceived } = getValues();
+    const usdAmount = 
+      typeof amountReceived === "number"
+        ? amountReceived
+        : parseFloat(String(amountReceived ?? "0")) || 0;
+
+    // TODO: Convert payout currency to source currency (USD) before limit check

Additionally, TransactionLimitModal.onClose should call refreshStatus() after the user completes KYC upgrades inside the modal, matching the pattern in ProfileDrawer. Otherwise, the parent form's limit checks remain stale if the user upgrades their tier.

🧹 Nitpick comments (6)
app/api/aggregator.ts (1)

776-830: Consider adding a strongly-typed payload interface.

The function correctly implements analytics tracking (without PII), error handling, and Bearer token authentication. However, the payload parameter is typed as any, which bypasses TypeScript's type safety.

Define a proper interface in app/types.ts for the SmileID payload structure and use it here:

// In app/types.ts (example structure - adjust based on actual SmileID requirements)
export type SmileIDPayload = {
  images: {
    selfie_image: string;
    id_image?: string;
  };
  partner_params: {
    user_id: string;
    job_id: string;
    job_type: number;
  };
  // ... other required fields
};

Then update the function signature:

 export const submitSmileIDData = async (
-  payload: any,
+  payload: SmileIDPayload,
   accessToken: string,
 ): Promise<SmileIDSubmissionResponse> => {
app/api/phone/verify-otp/route.ts (3)

87-123: Consider DB-side atomic increment for stricter attempt tracking.

The current approach computes verification.attempts + 1 client-side (using the value fetched at line 36) then writes it with a .lt('attempts', MAX_ATTEMPTS) boundary check. While the boundary prevents exceeding MAX_ATTEMPTS, concurrent requests with incorrect OTPs could read the same stale value and overwrite each other, causing some failed attempts to go uncounted (e.g., three concurrent requests all read attempts=1, all write attempts=2, but the user actually made three attempts).

For strict counting in this security-sensitive context, use a Postgres function that increments atomically:

const { data: updated, error: attemptsError } = await supabaseAdmin.rpc(
  'increment_otp_attempts',
  { 
    wallet_addr: walletAddress.toLowerCase(),
    max_attempts: MAX_ATTEMPTS 
  }
);

Create the function in your migration:

CREATE OR REPLACE FUNCTION increment_otp_attempts(wallet_addr text, max_attempts int)
RETURNS TABLE(attempts int) AS $$
  UPDATE user_kyc_profiles
  SET attempts = user_kyc_profiles.attempts + 1
  WHERE wallet_address = wallet_addr AND user_kyc_profiles.attempts < max_attempts
  RETURNING user_kyc_profiles.attempts;
$$ LANGUAGE sql;

This ensures every failed attempt is counted, even under concurrency, while retaining your existing boundary logic.


126-134: Optional: Replace any type with a specific type for updateData.

The any annotation bypasses TypeScript's type checking. Consider using a more specific type or defining an inline object type to maintain type safety:

-    const updateData: any = { 
+    const updateData: { 
+      verified: boolean; 
+      verified_at: string; 
+      tier?: number; 
+    } = { 
       verified: true, 
       verified_at: new Date().toISOString()
     };

Alternatively, if you have a UserKycProfile type, use Partial<Pick<UserKycProfile, 'verified' | 'verified_at' | 'tier'>>.


157-157: Optional: Return formatted phone number for UX consistency.

The response returns phoneNumber as received in the request body, which may vary in format. The send-otp endpoint returns validation.internationalFormat for consistent, readable display (e.g., "+1 234 567 890"). Consider aligning here:

-      phoneNumber: phoneNumber
+      phoneNumber: validation.internationalFormat

This ensures clients always receive a predictable, user-friendly format.

app/hooks/useKYCStatus.ts (1)

21-38: Consider defensive handling for unknown tiers and documenting per-instance state

getCurrentLimits assumes that any tier > 0 has a corresponding entry in KYC_TIERS, and canTransact relies on that. If the backend ever introduces a new tier or returns an unexpected value, KYC_TIERS[tier] will be undefined and .limits will throw at runtime.

You could defensively fall back to { monthly: 0 } when KYC_TIERS[tier] is missing, and/or clamp tier to the known set before indexing. Also, since each useKYCStatus call maintains its own React state (only the fetch guards are shared via window.__KYC_FETCH_GUARDS__), it’s worth keeping in mind that callers must invoke their own refreshStatus() to keep local state in sync after verification flows.

Also applies to: 71-87

app/components/TransactionLimitModal.tsx (1)

18-22: Wire transactionAmount into the UI or remove it for now

TransactionLimitModal accepts an optional transactionAmount and passes onVerified={handlePhoneVerified}, but:

  • transactionAmount is never used in the component (no copy or calculations reference it).
  • handlePhoneVerified ignores its phoneNumber argument and only closes the modals.

If the intent is to explain “We blocked a $X transaction because it exceeds your limit,” it would be good to surface transactionAmount in the copy (e.g., near the header or progress text). Otherwise, consider removing the prop and the unused phoneNumber parameter from handlePhoneVerified to keep the API tighter until you actually need it.

-interface TransactionLimitModalProps {
-    isOpen: boolean;
-    onClose: () => void;
-    transactionAmount?: number;
-}
+interface TransactionLimitModalProps {
+    isOpen: boolean;
+    onClose: () => void;
+    transactionAmount?: number; // use in copy once UX is finalized
+}
...
-    const handlePhoneVerified = async (phoneNumber: string) => {
+    const handlePhoneVerified = async () => {
         setIsPhoneModalOpen(false);
         onClose();
     };

If you do plan to show the blocked amount, now is a good time to thread it through the text.

Also applies to: 24-28, 60-63, 193-200

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8dc31f1 and 73e4bfc.

📒 Files selected for processing (7)
  • app/api/aggregator.ts (2 hunks)
  • app/api/phone/send-otp/route.ts (1 hunks)
  • app/api/phone/verify-otp/route.ts (1 hunks)
  • app/components/ProfileDrawer.tsx (1 hunks)
  • app/components/TransactionLimitModal.tsx (1 hunks)
  • app/hooks/useKYCStatus.ts (1 hunks)
  • app/pages/TransactionForm.tsx (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/api/phone/send-otp/route.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/pages/TransactionForm.tsx
  • app/components/TransactionLimitModal.tsx
  • app/api/phone/verify-otp/route.ts
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/pages/TransactionForm.tsx
  • app/components/TransactionLimitModal.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/pages/TransactionForm.tsx
  • app/components/TransactionLimitModal.tsx
🧬 Code graph analysis (4)
app/pages/TransactionForm.tsx (2)
app/hooks/useKYCStatus.ts (1)
  • useKYCStatus (58-191)
app/components/TransactionLimitModal.tsx (1)
  • TransactionLimitModal (24-217)
app/api/aggregator.ts (2)
app/types.ts (1)
  • SmileIDSubmissionResponse (240-248)
app/lib/server-analytics.ts (3)
  • trackServerEvent (130-175)
  • trackApiResponse (241-258)
  • trackBusinessEvent (362-375)
app/api/phone/verify-otp/route.ts (3)
app/api/phone/send-otp/route.ts (1)
  • POST (12-124)
app/lib/phone-verification.ts (1)
  • validatePhoneNumber (43-75)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/hooks/useKYCStatus.ts (1)
app/api/aggregator.ts (1)
  • fetchKYCStatus (307-353)
🔇 Additional comments (2)
app/api/aggregator.ts (1)

10-10: LGTM!

The import of SmileIDSubmissionResponse is correctly added to support the new submitSmileIDData function and matches the type definition in app/types.ts.

app/api/phone/verify-otp/route.ts (1)

1-169: LGTM—Solid OTP verification implementation with good error handling.

The endpoint properly validates inputs, normalizes phone numbers to E.164, splits fetch errors from not-found cases, handles race conditions with a boundary check, preserves higher tiers, and tracks analytics consistently. The improvements from past reviews (error handling on increment, fetch error differentiation) are well-integrated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
app/pages/TransactionForm.tsx (2)

272-287: Ensure isUserVerified is updated after completing KYC inside TransactionLimitModal

In this component, setIsUserVerified(true) is only invoked in the checkKycStatus effect when fetchKYCStatus returns "success" (primarily on mount/address change). After wiring buttonAction to open TransactionLimitModal instead of this component’s KycModal, TransactionLimitModal no longer receives setIsUserVerified, so KYC completion inside that modal won’t directly flip isUserVerified here unless the parent updates it from some other source (e.g., a global useKYCStatus listener).

That can leave this form thinking the user is unverified during the same session (affecting useSwapButton behavior and the isUserVerified gate for RecipientDetailsForm), even though KYC has just succeeded.

Please verify that:

  • The parent does, in fact, update isUserVerified in response to KYC success (e.g., via useKYCStatus + refreshStatus), or
  • You pass a callback (or derive isUserVerified from tier) such that KYC completion in TransactionLimitModal reliably updates this prop without requiring a full reload.

If not, consider plumbing setIsUserVerified into TransactionLimitModal or deriving isUserVerified directly from useKYCStatus to avoid this desync.

Also applies to: 861-868, 898-899


78-79: Remove now-unreachable KycModal state/rendering from TransactionForm

With buttonAction now using () => setIsLimitModalOpen(true) as the KYC/limits entry point, there’s no remaining code path in this component that sets isKycModalOpen to true. The KycModal inside the AnimatePresence block therefore appears unreachable from TransactionForm itself, while TransactionLimitModal is intended to orchestrate the KYC flow.

To reduce confusion and keep the modal graph focused, consider removing:

  • isKycModalOpen state, and
  • The <AnimatedModal><KycModal … /></AnimatedModal> block here,

and letting TransactionLimitModal own the embedded KYC experience end‑to‑end.

Also applies to: 847-858, 898-899

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73e4bfc and 37753e1.

📒 Files selected for processing (2)
  • app/components/ProfileDrawer.tsx (1 hunks)
  • app/pages/TransactionForm.tsx (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/components/ProfileDrawer.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/pages/TransactionForm.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/pages/TransactionForm.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/pages/TransactionForm.tsx
🔇 Additional comments (1)
app/pages/TransactionForm.tsx (1)

72-86: Clarify canTransact input units and TransactionLimitModal transactionAmount semantics

handleSwap labels amountReceived as usdAmount when passing it to canTransact, but in this form amountReceived represents the payout amount in the selected currency, not necessarily USD. If the backend expects USD-equivalent values for limit checking, this will incorrectly enforce (or fail to enforce) limits for non-USD payouts and propagate a misaligned amount into blockedTransactionAmount / TransactionLimitModal.transactionAmount.

Verify:

  • Whether canTransact and backend limits expect raw USD or the local currency amount
  • Whether amountReceived is normalized to USD before calling canTransact
  • Whether TransactionLimitModal assumes transactionAmount is in USD or just displays the raw value

If limits are USD-based, explicitly convert to USD (or pass both amount and currency to the API) rather than relying on an implicit assumption that amountReceived is already normalized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (3)
app/lib/smileID.ts (1)

84-91: Avoid logging potentially sensitive SmileID response data.

The error handler logs error.response?.data directly, which per SmileID docs may contain job metadata, presigned URLs with tokens, or reference IDs that should be treated as sensitive. This was flagged in a prior review.

Restrict logging to high-level fields only:

   } catch (error: any) {
     console.error('SmileID API Error:', {
       status: error.response?.status,
       statusText: error.response?.statusText,
-      data: error.response?.data,
       message: error.message,
     });
     throw error;
   }
app/components/TransactionLimitModal.tsx (1)

60-63: Missing status refresh after phone verification.

handlePhoneVerified closes both modals but doesn't call refreshStatus(). After successful phone verification, the KYC tier should update from 0 to 1, but without a refresh, the parent component may show stale tier information.

     const handlePhoneVerified = async (phoneNumber: string) => {
+        await refreshStatus();
         setIsPhoneModalOpen(false);
         onClose();
     };
app/components/ProfileDrawer.tsx (1)

302-350: KYC tier card & upgrade CTA logic looks correct and aligns with backend semantics

The “Current Tier Status” and “Increase limit” CTA conditions now line up with the documented behavior:

  • tier >= 1 gates the current tier card.
  • The upgrade CTA is shown only for Tier 1 (tier < 2 && !isFullyVerified), so Tier 2 users don’t see an invalid “upgrade” to a non‑existent tier.
  • The next tiers list is restricted to tierData.level > tier, so only higher tiers are shown.

This matches the tiered KYC flow (Tier 1 = phone‑verified, Tier 2 = full KYC) and avoids the undefined next‑tier issue from the earlier version.

Also applies to: 354-356

🧹 Nitpick comments (5)
app/lib/smileID.ts (2)

29-34: Consider adding types for images and partner_params.

Both parameters are typed as any, which reduces type safety. When feasible, introduce minimal interfaces (e.g., SmileIdPartnerParams, SmileIdImage) to catch shape mismatches at compile time.


62-64: Minor collision risk in job_id generation.

The job ID uses Date.now() and the first 8 characters of the wallet address. If a user submits multiple jobs within the same millisecond, IDs could collide. Consider adding a random suffix or using a UUID library if uniqueness is critical.

+  import { randomBytes } from 'crypto';
   // Generate unique IDs
   const timestamp = Date.now();
-  const job_id = `job-${timestamp}-${walletAddress.slice(0, 8)}`;
+  const job_id = `job-${timestamp}-${walletAddress.slice(0, 8)}-${randomBytes(4).toString('hex')}`;
app/components/PhoneVerificationModal.tsx (1)

501-513: State reset logic is duplicated from handleClose.

The verified success callback duplicates the state reset logic from handleClose (lines 233-239). Extract to a shared helper to reduce maintenance burden.

+  const resetState = () => {
+    setStep(STEPS.ENTER_PHONE);
+    setPhoneNumber('');
+    setFormattedPhone('');
+    setOtpCode('');
+    setAttemptsRemaining(3);
+    setIsCountryDropdownOpen(false);
+    setCountrySearch('');
+  };
+
   const handleClose = () => {
     onClose();
-    setStep(STEPS.ENTER_PHONE);
-    setPhoneNumber('');
-    // ... rest of resets
+    resetState();
   };
app/components/ProfileDrawer.tsx (2)

132-187: Tier section UX: tighten icon logic and arrow state

The tier accordion works, but there are a couple of polish opportunities:

  1. Arrow icon rotation – the className conditional currently doesn’t change anything:
className={classNames(
  "size-4 ... transition-transform",
  isExpanded ? "" : ""
)}

You likely meant to rotate when expanded:

className={classNames(
  "size-4 ... transition-transform",
  isExpanded ? "rotate-180" : "rotate-0"
)}
  1. Requirement → icon mapping – the string‑based checks (includes('number'), includes('verification'), includes('ID')) are brittle; copy changes will silently break icon selection. If possible, make requirements structured (e.g. { label, type: 'phone' | 'liveness' | 'id' }) and map type to icons instead of parsing the text.

Not strictly blocking, but it will make this section more maintainable as copy evolves.


216-236: Dialog & refresh behavior: generally solid, only minor resilience nits

Overall the modal wiring looks good:

  • Headless UI Dialog is controlled via open={isOpen} and onClose={onClose} and wrapped in AnimatePresence.
  • Close button and backdrop both call onClose, which is expected.
  • TransactionLimitModal triggers refreshStatus() on close, ensuring the drawer reflects updated limits.

Two small resilience improvements to consider:

  1. Handle refreshStatus errors – in both the useEffect and onClose callback you ignore failures. Adding a .catch(() => toast.error("Unable to refresh verification status")) (or at least console.error) would make issues more visible.

  2. Optional: use Dialog.Title for accessibility – wrapping the “Profile” heading with <Dialog.Title> would improve screen reader labeling:

<Dialog.Title as="h2" className="text-lg font-semibold ...">
  Profile
</Dialog.Title>

Neither is blocking, but they’ll make this flow more robust and accessible.

Also applies to: 239-251, 367-373

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37753e1 and d0087a2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • app/components/PhoneVerificationModal.tsx (1 hunks)
  • app/components/ProfileDrawer.tsx (1 hunks)
  • app/components/TransactionLimitModal.tsx (1 hunks)
  • app/hooks/useKYCStatus.ts (1 hunks)
  • app/hooks/useSwapButton.ts (2 hunks)
  • app/lib/smileID.ts (1 hunks)
  • app/pages/TransactionForm.tsx (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/hooks/useKYCStatus.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/pages/TransactionForm.tsx
  • app/components/TransactionLimitModal.tsx
  • app/components/ProfileDrawer.tsx
  • app/components/PhoneVerificationModal.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/pages/TransactionForm.tsx
  • app/components/TransactionLimitModal.tsx
  • app/components/ProfileDrawer.tsx
  • app/components/PhoneVerificationModal.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/pages/TransactionForm.tsx
  • app/components/TransactionLimitModal.tsx
  • app/components/PhoneVerificationModal.tsx
🧬 Code graph analysis (1)
app/components/PhoneVerificationModal.tsx (2)
app/lib/countries.ts (4)
  • Country (1-6)
  • fetchCountries (15-60)
  • searchCountries (95-104)
  • getPopularCountries (88-90)
app/components/index.ts (5)
  • fadeInOut (21-21)
  • primaryBtnClasses (47-47)
  • AnimatedComponent (15-15)
  • slideInOut (19-19)
  • secondaryBtnClasses (48-48)
🔇 Additional comments (7)
app/hooks/useSwapButton.ts (1)

101-118: LGTM! Clean refactor to integrate the new limit modal.

The logic correctly gates the limit modal on !hasInsufficientBalance, ensuring users with insufficient funds are directed to fund their wallet first rather than being shown the verification flow. The parameter rename from setIsKycModalOpen to setIsLimitModalOpen aligns with the new modal naming convention.

app/components/PhoneVerificationModal.tsx (2)

400-401: Phone masking regex may not handle all formats correctly.

The regex formattedPhone.replace(/(\+\d+\s+\d{3})[\s\d]+(\d{2})/, '$1**$2') assumes a specific format with spaces. If formattedPhone uses different separators (e.g., dashes, no separators), the masking won't work as intended.


203-228: handleResendOtp omits the name field present in initial OTP request.

handlePhoneSubmit sends { phoneNumber, walletAddress, name }, but handleResendOtp only sends { phoneNumber, walletAddress }. If the backend uses name for OTP metadata or logging, this inconsistency could cause issues. Verify whether the backend requires name on resend.

#!/bin/bash
# Check if send-otp route expects name field on all requests
rg -nA 20 'export.*POST' app/api/phone/send-otp/route.ts | head -40
app/pages/TransactionForm.tsx (2)

271-282: LGTM! KYC status refresh on wallet changes.

The effect correctly triggers refreshStatus() when the wallet context changes, ensuring KYC state stays synchronized.


839-846: LGTM! Modal closes with status refresh.

The onClose handler properly refreshes KYC status after the modal closes, addressing the stale state concern from the prior review.

app/components/TransactionLimitModal.tsx (2)

112-126: LGTM! Progress bar guards against division by zero.

The progress bar width calculation properly handles the case when currentLimits.monthly is 0, avoiding NaN/Infinity.


42-47: refreshStatus is properly memoized and stable; no infinite loop concern.

refreshStatus is correctly wrapped with useCallback in the useKYCStatus hook with appropriate dependencies. The fetchGuards global object pattern ensures stable callback identities across renders, preventing infinite effect loops in TransactionLimitModal.

Comment on lines +377 to +378
By clicking "Verify and start", you consent to recieving transactional text messages for notifications and alerts from Noblocks. Reply STOP to opt out. you agree to our <a href="https://paycrest.io/privacy-policy" className='text-lavender-600'>Privacy Policy and terms & conditions.</a>.
</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: "recieving" should be "receiving".

User-facing text has a spelling error.

-By clicking "Verify and start", you consent to recieving transactional text messages for notifications and alerts from Noblocks.
+By clicking "Verify and start", you consent to receiving transactional text messages for notifications and alerts from Noblocks.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
By clicking "Verify and start", you consent to recieving transactional text messages for notifications and alerts from Noblocks. Reply STOP to opt out. you agree to our <a href="https://paycrest.io/privacy-policy" className='text-lavender-600'>Privacy Policy and terms & conditions.</a>.
</p>
By clicking "Verify and start", you consent to receiving transactional text messages for notifications and alerts from Noblocks. Reply STOP to opt out. you agree to our <a href="https://paycrest.io/privacy-policy" className='text-lavender-600'>Privacy Policy and terms & conditions.</a>.
</p>
🤖 Prompt for AI Agents
In app/components/PhoneVerificationModal.tsx around lines 377 to 378, the
user-facing string contains a spelling error: change "recieving" to "receiving"
in the paragraph text; update the JSX string accordingly (ensure the rest of the
sentence and anchor remain unchanged and run a quick lint/format check to
confirm no formatting issues).

Comment on lines +43 to +45
const currentLimits = getCurrentLimits();
const monthlyProgress = (transactionSummary.monthlySpent / currentLimits.monthly) * 100;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against zero / missing monthly limit in progress calculation

monthlyProgress divides by currentLimits.monthly without guarding against 0 or undefined. If an unverified tier (or misconfig) sets a monthly limit of 0, you’ll get NaN (0/0) and end up with style={{ width: 'NaN%' }}, which is invalid CSS.

Consider a defensive clamp:

const monthlyLimit = currentLimits.monthly || 0;
const monthlyProgress =
  monthlyLimit > 0
    ? (transactionSummary.monthlySpent / monthlyLimit) * 100
    : 0;

This keeps the bar stable even if limits are missing or zeroed.

🤖 Prompt for AI Agents
In app/components/ProfileDrawer.tsx around lines 43-45, the calculation for
monthlyProgress divides by currentLimits.monthly without guarding against zero
or undefined which can produce NaN and invalid CSS; fix by reading the monthly
limit into a local variable (defaulting to 0), then compute monthlyProgress only
if that limit is > 0, otherwise set progress to 0 (and optionally clamp the
computed percent to the 0-100 range) so the resulting style width is always a
valid percentage.

Comment on lines +61 to +70
const { linkEmail } = useLinkAccount({
onSuccess: ({ user }) => {
toast.success(`${user.email} linked successfully`);
},
onError: () => {
toast.error("Error linking account", {
description: "You might have this email linked already",
});
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -t f "ProfileDrawer.tsx" --exec cat -n {}

Repository: paycrest/noblocks

Length of output: 24307


🏁 Script executed:

rg -t ts -t tsx "useLinkAccount" --type-list | head -20

Repository: paycrest/noblocks

Length of output: 450


🏁 Script executed:

rg -t ts -t tsx "interface.*User|type.*User" --max-count=10 -A 5

Repository: paycrest/noblocks

Length of output: 88


🏁 Script executed:

# Look for Privy type definitions or where useLinkAccount is defined
rg "from.*privy" -A 5 -B 5

Repository: paycrest/noblocks

Length of output: 23200


🏁 Script executed:

rg "useLinkAccount" -A 10 --max-count=20

Repository: paycrest/noblocks

Length of output: 3053


Fix email success toast to use user.email.address instead of user.email

In the account card you render the email as user.email.address (line 267), but in the onSuccess callback you interpolate user.email directly (line 63):

onSuccess: ({ user }) => {
  toast.success(`${user.email} linked successfully`);
}

Since user.email is an object, this will produce "[object Object] linked successfully". Use the same field as the UI:

onSuccess: ({ user }) => {
  toast.success(`${user.email?.address} linked successfully`);
}

This also applies to lines 264-268 where the email is displayed in the account card.

🤖 Prompt for AI Agents
In app/components/ProfileDrawer.tsx around lines 61 to 70 (and related UI at
lines 264 to 268), the success toast currently interpolates user.email which is
an object and yields "[object Object]"; change the onSuccess toast to use
user.email?.address (or a safe accessor) so it shows the actual email string,
and likewise update the account card rendering at lines 264-268 to render
user.email?.address with optional chaining/guarding to avoid runtime errors when
email is undefined.

Comment on lines +72 to +80
const handleCopyAddress = () => {
if (walletAddress) {
navigator.clipboard.writeText(walletAddress);
setIsAddressCopied(true);
toast.success("Address copied to clipboard");
setTimeout(() => setIsAddressCopied(false), 2000);
}
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Improve wallet address UX: hide when missing and handle copy errors

Right now the wallet row always renders and shortenAddress(walletAddress ?? "", 10) is called even when there is no walletAddress, and handleCopyAddress ignores clipboard errors:

  • Empty/undefined addresses will still render some placeholder from shortenAddress, which is confusing.
  • navigator.clipboard.writeText is not available or may reject in some environments without any feedback.

Consider:

const hasWallet = Boolean(walletAddress);

{hasWallet && (
  <div className="flex items-center justify-between w-full">
    <p className="text-sm font-light ...">
      {shortenAddress(walletAddress!, 10)}
    </p>
    <button ... onClick={handleCopyAddress}>...</button>
  </div>
)}

And in handleCopyAddress:

if (walletAddress && navigator.clipboard?.writeText) {
  navigator.clipboard.writeText(walletAddress)
    .then(() => {
      setIsAddressCopied(true);
      toast.success("Address copied to clipboard");
      setTimeout(() => setIsAddressCopied(false), 2000);
    })
    .catch(() => {
      toast.error("Unable to copy address");
    });
}

This avoids odd UI when no wallet is present and makes copy behavior more robust.

Also applies to: 279-296

🤖 Prompt for AI Agents
In app/components/ProfileDrawer.tsx around lines 72-80 (and also apply the same
fix at 279-296): the wallet row currently renders even when walletAddress is
empty and calls shortenAddress on a missing value, and handleCopyAddress calls
navigator.clipboard.writeText without feature detection or error handling. Wrap
the wallet UI in a hasWallet conditional (e.g., Boolean(walletAddress)) so
shortenAddress is only called when walletAddress exists; update
handleCopyAddress to check walletAddress and navigator.clipboard?.writeText, use
the returned promise to set isAddressCopied and show success toast on resolve,
and catch errors to show an error toast (and do not call setIsAddressCopied on
failure). Ensure any timeout to clear isAddressCopied still runs only after a
successful copy.

Comment on lines +50 to +54
useEffect(() => {
if (isOpen && !isLoading && ((tier < 1 && !phoneNumber) || tier === undefined)) {
setIsPhoneModalOpen(true);
}
}, [isOpen, isLoading, tier]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing phoneNumber in dependency array.

The condition references phoneNumber but the dependency array only includes [isOpen, isLoading, tier]. If phoneNumber changes after initial load, the auto-open logic won't re-evaluate.

     useEffect(() => {
         if (isOpen && !isLoading && ((tier < 1 && !phoneNumber) || tier === undefined)) {
             setIsPhoneModalOpen(true);
         }
-    }, [isOpen, isLoading, tier]);
+    }, [isOpen, isLoading, tier, phoneNumber]);
🤖 Prompt for AI Agents
In app/components/TransactionLimitModal.tsx around lines 50 to 54, the useEffect
condition reads phoneNumber but the dependency array omits it, so changes to
phoneNumber won't re-evaluate the auto-open logic; add phoneNumber to the
dependency array (e.g. [isOpen, isLoading, tier, phoneNumber]) so the effect
runs when phoneNumber changes and ensure the effect logic still behaves
correctly with the added dependency.

Comment on lines +208 to +211
<KycModal
setIsKycModalOpen={setIsKycModalOpen}
setIsUserVerified={() => { }}
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

setIsUserVerified is a no-op—KYC completion has no effect.

KycModal receives setIsUserVerified={() => {}}, so successful KYC verification won't update any state. This breaks the upgrade flow: after completing full KYC, the user's verified status won't be reflected.

Consider passing a meaningful callback that refreshes status and closes the modal:

     <KycModal
         setIsKycModalOpen={setIsKycModalOpen}
-        setIsUserVerified={() => { }}
+        setIsUserVerified={async () => {
+            await refreshStatus();
+            setIsKycModalOpen(false);
+            onClose();
+        }}
     />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<KycModal
setIsKycModalOpen={setIsKycModalOpen}
setIsUserVerified={() => { }}
/>
<KycModal
setIsKycModalOpen={setIsKycModalOpen}
setIsUserVerified={async () => {
await refreshStatus();
setIsKycModalOpen(false);
onClose();
}}
/>

Comment on lines +440 to +452

// Calculate the USD amount for transaction limit checking
const formData = getValues();
const usdAmount = formData.amountReceived || 0;

// Check transaction limits based on KYC tier
const limitCheck = canTransact(usdAmount);

if (!limitCheck.allowed) {
setBlockedTransactionAmount(usdAmount);
setIsLimitModalOpen(true);
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the canTransact function implementation and what it expects
rg -nA 15 'canTransact' app/hooks/useKYCStatus.ts

Repository: paycrest/noblocks

Length of output: 1268


🏁 Script executed:

#!/bin/bash
# Check the form field definitions to understand what amountReceived and amountSent represent
rg -nB 5 -A 5 'amountReceived\|amountSent' app/pages/TransactionForm.tsx | head -100

Repository: paycrest/noblocks

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check how tier limits are defined
rg -nA 10 'tier.*limit\|transactionLimit' app/hooks/useKYCStatus.ts

Repository: paycrest/noblocks

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check getRemainingLimits implementation
rg -nB 5 -A 20 'getRemainingLimits' app/hooks/useKYCStatus.ts

Repository: paycrest/noblocks

Length of output: 3049


🏁 Script executed:

#!/bin/bash
# Check transaction limits and tier definitions
rg -nB 2 -A 10 'TransactionLimits\|TIER.*LIMIT\|tier.*Limit' app/hooks/useKYCStatus.ts

Repository: paycrest/noblocks

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check the form schema or validation to understand field definitions
fd -t f 'TransactionForm' app/pages/ --exec grep -l 'amountReceived\|amountSent' {} \;

Repository: paycrest/noblocks

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Look at the actual form field setup more directly
sed -n '1,150p' app/pages/TransactionForm.tsx | grep -n 'amountReceived\|amountSent\|useForm'

Repository: paycrest/noblocks

Length of output: 237


🏁 Script executed:

#!/bin/bash
# Get the full getCurrentLimits implementation
rg -nB 5 -A 15 'getCurrentLimits.*useCallback' app/hooks/useKYCStatus.ts

Repository: paycrest/noblocks

Length of output: 745


🏁 Script executed:

#!/bin/bash
# Search for tier limit constants or definitions
rg -n 'TIER_LIMITS\|tier.*limit\|500.*1000.*5000' app/hooks/useKYCStatus.ts

Repository: paycrest/noblocks

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check the form schema to understand amountReceived vs amountSent context
rg -nB 10 -A 3 'amountReceived.*:.*number\|amountSent.*:.*number' app/pages/TransactionForm.tsx

Repository: paycrest/noblocks

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check the backend API to understand what monthlySpent represents
find app -type f -name '*.ts' -o -name '*.tsx' | xargs grep -l 'transaction-summary' | head -5

Repository: paycrest/noblocks

Length of output: 127


🏁 Script executed:

#!/bin/bash
# Find KYC_TIERS definition
rg -n 'KYC_TIERS\s*=' app/hooks/useKYCStatus.ts

Repository: paycrest/noblocks

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Search for KYC_TIERS in the entire codebase
rg -n 'KYC_TIERS' app/

Repository: paycrest/noblocks

Length of output: 1224


🏁 Script executed:

#!/bin/bash
# Check the transaction-summary API route
cat -n app/api/kyc/transaction-summary/route.ts | head -80

Repository: paycrest/noblocks

Length of output: 3218


🏁 Script executed:

#!/bin/bash
# Get the full KYC_TIERS definition
sed -n '21,50p' app/hooks/useKYCStatus.ts

Repository: paycrest/noblocks

Length of output: 634


🏁 Script executed:

#!/bin/bash
# Check form field definitions and labels for amountSent and amountReceived
rg -nB 5 -A 5 'name.*amountSent\|name.*amountReceived' app/pages/TransactionForm.tsx | head -60

Repository: paycrest/noblocks

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Look at the transaction form to understand field purposes
sed -n '440,460p' app/pages/TransactionForm.tsx

Repository: paycrest/noblocks

Length of output: 692


Use amountSent instead of amountReceived for transaction limit checking—tier limits are in USD, not local currency.

The KYC tier limits (Tier 1: $100, Tier 2: $15,000) and monthlySpent tracking from the backend are both in stablecoin/USD. However, formData.amountReceived is the local currency amount (NGN, KES, etc.) that the recipient gets, not the USD amount being sent. Using it for canTransact() causes incorrect limit checks.

Change line 443 from:

-    const usdAmount = formData.amountReceived || 0;
+    const usdAmount = formData.amountSent || 0;

amountSent is the stablecoin amount and matches the currency of the tier limits and monthlySpent tracking.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Calculate the USD amount for transaction limit checking
const formData = getValues();
const usdAmount = formData.amountReceived || 0;
// Check transaction limits based on KYC tier
const limitCheck = canTransact(usdAmount);
if (!limitCheck.allowed) {
setBlockedTransactionAmount(usdAmount);
setIsLimitModalOpen(true);
return;
}
// Calculate the USD amount for transaction limit checking
const formData = getValues();
const usdAmount = formData.amountSent || 0;
// Check transaction limits based on KYC tier
const limitCheck = canTransact(usdAmount);
if (!limitCheck.allowed) {
setBlockedTransactionAmount(usdAmount);
setIsLimitModalOpen(true);
return;
}
🤖 Prompt for AI Agents
In app/pages/TransactionForm.tsx around lines 440 to 452, the code uses
formData.amountReceived (local currency) to check KYC USD tier limits, causing
incorrect limit checks; replace usage with formData.amountSent (the
stablecoin/USD amount) by reading const formData = getValues(); const usdAmount
= formData.amountSent || 0; then call canTransact(usdAmount) and
setBlockedTransactionAmount(usdAmount) so the limit check and blocked amount
align with backend monthlySpent and tier USD values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants