Skip to content

Conversation

@franm91
Copy link
Member

@franm91 franm91 commented Jan 30, 2026

closes #470


Open with Devin

Summary by CodeRabbit

  • New Features

    • Manteca fiat on‑ramp: provider selection, quotes, deposit details, copy‑to‑clipboard, and support for ARS/BRL/USD.
  • New Screens

    • End‑to‑end onboarding/KYC, status, ramp/quote, and deposit detail screens plus Add Fiat buttons.
  • UX Improvements

    • Country‑aware provider listing, loading states, contextual routing, and streamlined Add Funds flow.
  • Documentation

    • Spanish translations added for onboarding and deposit flows.

@changeset-bot
Copy link

changeset-bot bot commented Jan 30, 2026

🦋 Changeset detected

Latest commit: c920746

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Jan 30, 2026

Walkthrough

Adds Manteca ramp/onramp: new Add Funds routes and re-exported route modules, five Add Funds UI components (Onboard, Kyc, Ramp, Status, AddFiatButton), KYC/ramp client+server APIs and Persona Manteca KYC flow, currency utilities, and Spanish translations.

Changes

Cohort / File(s) Summary
Changeset
.changeset/shiny-points-eat.md
New changeset declaring a patch for @exactly/mobile with summary "✨ implement manteca ramp".
Add Funds routes (layout + re-exports)
src/app/(main)/add-funds/_layout.tsx, src/app/(main)/add-funds/kyc.tsx, src/app/(main)/add-funds/onboard.tsx, src/app/(main)/add-funds/ramp.tsx, src/app/(main)/add-funds/status.tsx
Expose new stack routes and add thin re-export route modules for onboard, kyc, ramp, and status.
Add Funds components
src/components/add-funds/AddFiatButton.tsx, src/components/add-funds/AddFunds.tsx, src/components/add-funds/Kyc.tsx, src/components/add-funds/Onboard.tsx, src/components/add-funds/Ramp.tsx, src/components/add-funds/Status.tsx
Add AddFiatButton; implement Manteca KYC UI/mutation and startMantecaKYC flow, onboarding (startRampOnboarding), ramp quote/account detail view with copy-to-clipboard, status screens; AddFunds integrates country/provider fetching to render fiat options.
Home / navigation adjustments
src/components/home/Home.tsx, src/components/home/HomeActions.tsx
Change Home KYC query to getKYCStatus("basic") and simplify deposit action to always navigate to /add-funds (removed method-based branching).
Persona & server APIs
src/utils/persona.ts, src/utils/server.ts
Add startMantecaKYC() and MantecaKYCResult type; extend getKYCTokens/getKYCStatus to accept "manteca"; add ramp APIs/types (getRampProviders, getRampQuote, startRampOnboarding, ProviderInfo, RampProvider, etc.).
Currency utilities
src/utils/currencies.ts
Add currencyMap (ARS/BRL/USD), Currency type, CurrencySchema, and isValidCurrency validator.
Localization
src/i18n/es.json
Add Spanish translations for onboarding/KYC/ramp UI and fix formatting.
Misc / manifest
package.json
Minor manifest edits.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant App as Mobile App
    participant Server as API Server
    participant Manteca as Manteca KYC
    participant Ramp as Ramp Provider

    User->>App: Open Add Funds
    App->>Server: getKYCStatus("basic")
    Server-->>App: kyc status (+ countryCode)
    App->>Server: getRampProviders(countryCode)
    Server-->>App: providers (e.g., manteca)
    User->>App: Start onboard for currency
    App->>Server: getKYCStatus("manteca")
    Server-->>App: manteca kyc status
    alt KYC not started
        App->>Manteca: startMantecaKYC()
        Manteca-->>App: KYC result (complete/cancel/error)
    end
    alt KYC complete
        App->>Server: startRampOnboarding({provider:"manteca"})
        Server->>Ramp: init onboarding
        Ramp-->>Server: provider status
        Server-->>App: provider status (ACTIVE/ONBOARDING)
        App->>Server: getRampQuote(currency)
        Server->>Ramp: request quote/account
        Ramp-->>Server: quote + account
        Server-->>App: quote + account details
        App-->>User: show ramp details / account info
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • dieguezguille
  • nfmelendez
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '✨ app: implement manteca ramp' clearly describes the main change of implementing Manteca ramp functionality across the application.
Linked Issues check ✅ Passed The PR closes issue #470 (ui: ramp manteca) and the changeset implements all core Manteca ramp features: KYC flow, onboarding, ramp quotes, status tracking, and UI components.
Out of Scope Changes check ✅ Passed All changes relate to implementing the Manteca ramp feature. Minor updates to existing queries and removal of simplified routing logic are necessary supporting changes for the new ramp implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 ramp

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Reviews will stop working after February 8, 2026 if the new IP is not added to your allowlist.


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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @franm91, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new feature: the Manteca fiat on-ramp. It provides a comprehensive flow for users to add funds by converting fiat currencies into on-chain USDC. The changes encompass new UI screens for onboarding, KYC verification, status tracking, and the ramp details, along with the necessary backend integrations to support these operations.

Highlights

  • New Manteca Ramp Integration: Implemented the Manteca fiat on-ramp solution, enabling users to convert fiat currencies to on-chain USDC directly within the application.
  • Expanded Add Funds Flow: Introduced new dedicated screens for the add-funds process, including 'onboard', 'kyc', 'status', and 'ramp', providing a structured user journey.
  • Dynamic Fiat On-Ramp Options: Integrated a new AddFiatButton component to dynamically display available fiat on-ramp options based on user's country and provider status.
  • Enhanced KYC Management: Updated the KYC status retrieval and initiation logic to support distinct 'basic' and 'manteca' verification flows, leveraging the Persona SDK.
  • New SVG Assets: Added several new SVG image assets for various currencies (ARS, BRL, USD) and status indicators (denied, face-id) to enrich the UI.
  • Simplified Deposit Navigation: Streamlined the deposit action in HomeActions to directly navigate to the main add-funds screen, improving user experience.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View issue and 6 additional flags in Devin Review.

Open in Devin Review

const router = useRouter();

const { currency } = useLocalSearchParams<{ currency: string }>();
const countryCode = queryClient.getQueryData<string>(["user", "country"]) ?? "";

Choose a reason for hiding this comment

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

🟡 Stale countryCode captured in closure leads to incorrect provider status query

The countryCode is read synchronously from queryClient.getQueryData at component render time and captured in the completeOnboarding closure. If a user navigates directly to /add-funds/onboard or /add-funds/kyc (via deep link, browser refresh, or back navigation) without first visiting the AddFunds page, the countryCode will be an empty string because only AddFunds.tsx:41 calls getKYCStatus("basic", true) to populate it.

Click to expand

Code flow issue

In Onboard.tsx:31 and Kyc.tsx:27:

const countryCode = queryClient.getQueryData<string>(["user", "country"]) ?? "";

This value is then used in completeOnboarding() at Onboard.tsx:70-73 and Kyc.tsx:54-57:

const providers = await queryClient.fetchQuery({
  queryKey: ["ramp", "providers", countryCode],
  queryFn: () => getRampProviders(countryCode),
  staleTime: 0,
});

Impact

When countryCode is empty:

  1. The server's getSupportedByCountry("") returns an empty currencies array (server/utils/ramps/manteca.ts:683-684)
  2. The query key ["ramp", "providers", ""] differs from the one used in AddFunds.tsx with the correct country code
  3. This may cause cache misses and inconsistent data between components

Recommendation: Use a useQuery hook to fetch the country code reactively (similar to how AddFunds.tsx does it), or ensure the country code is passed via route params when navigating to these screens.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new fiat on-ramp feature using 'Manteca' as a provider. This includes new screens for onboarding, KYC, and displaying ramp details, along with the necessary components, assets, and API utility functions. The changes are well-structured, but there are a few areas that could be improved for clarity, maintainability, and robustness. Specifically, the Persona KYC initiation logic is duplicated, some useQuery data flow is indirect, and depositInfo extraction could be cleaner.

Comment on lines +38 to +46
const { data: countryCode } = useQuery({
queryKey: ["user", "country"],
queryFn: async () => {
await getKYCStatus("basic", true);
return queryClient.getQueryData<string>(["user", "country"]) ?? "";
},
staleTime: (query) => (query.state.data ? Infinity : 0),
retry: false,
});

Choose a reason for hiding this comment

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

medium

This useQuery hook for countryCode has a confusing implementation. The queryFn calls getKYCStatus("basic", true), which has a side effect of setting the country code in the query cache via a response header, and then the queryFn returns a value read from that same cache. This indirect data flow is hard to follow. A clearer approach would be to refactor getKYCStatus to return the country code in its response body, allowing this query to directly return the value without relying on side effects.

Comment on lines +70 to +77
const beneficiaryName = depositInfo && "beneficiaryName" in depositInfo ? depositInfo.beneficiaryName : undefined;
const depositAddress =
depositInfo?.network === "ARG_FIAT_TRANSFER"
? depositInfo.cbu
: depositInfo?.network === "PIX"
? depositInfo.pixKey
: undefined;
const depositAlias = depositInfo?.network === "ARG_FIAT_TRANSFER" ? depositInfo.depositAlias : undefined;

Choose a reason for hiding this comment

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

medium

The logic to extract properties like beneficiaryName, depositAddress, and depositAlias from depositInfo is spread across multiple lines with optional chaining and in checks. This can be hard to follow and maintain. Since depositInfo appears to be a discriminated union based on the network property, using a switch statement or a helper function would make this logic cleaner, more robust, and easier to read.

@sentry
Copy link

sentry bot commented Jan 30, 2026

Codecov Report

❌ Patch coverage is 8.00000% with 92 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.05%. Comparing base (1608e9c) to head (ef44b50).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/utils/persona.ts 0.00% 60 Missing ⚠️
src/utils/server.ts 18.42% 31 Missing ⚠️
src/components/home/HomeActions.tsx 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main     #704       +/-   ##
===========================================
+ Coverage   15.06%   44.05%   +28.98%     
===========================================
  Files          39      176      +137     
  Lines        1786     5328     +3542     
  Branches      454     1634     +1180     
===========================================
+ Hits          269     2347     +2078     
- Misses       1471     2869     +1398     
- Partials       46      112       +66     
Flag Coverage Δ
e2e 44.05% <8.00%> (+28.98%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@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

🤖 Fix all issues with AI agents
In `@src/components/add-funds/AddFiatButton.tsx`:
- Around line 10-13: The AddFiatButtonProperties type declares currency as a
plain string which weakens type safety against currencyMap keys; import the
Currency type from ../../utils/currencies and change the property signature in
AddFiatButtonProperties from currency: string to currency: Currency so the
component's currency prop is strongly typed to the known currency keys (update
any usages of AddFiatButtonProperties/AddFiatButton to satisfy the new type if
necessary).

In `@src/components/add-funds/AddFunds.tsx`:
- Line 113: In AddFunds.tsx replace the hardcoded gap={16} on the YStack with
the appropriate design token (e.g., the app's spacing token such as
spacing.medium or $4) used across the codebase; update the YStack prop to use
that token (and import the token if needed) so styling relies on predefined
design tokens rather than a raw number, then run the linter/formatter to ensure
the prop type matches the token format.
- Around line 107-111: In the AddFunds component JSX where the fiat options are
rendered (the block using countryCode and isPending to decide between the
Skeleton and options), change the condition from countryCode && isPending to
show the skeleton when either the providers are loading or the country code is
not yet available (e.g., use isPending || !countryCode or !countryCode ||
isPending) so the loading skeleton is displayed while countryCode is being
fetched as well as while providers are loading.

In `@src/components/add-funds/Kyc.tsx`:
- Around line 99-101: The FaceId SVG is receiving string percentages for
dimensions which don't work in React Native SVG; update the FaceId usage in
Kyc.tsx (the View + FaceId block) to pass numeric width/height props (e.g.,
width={200} height={200} or width={iconSize} height={iconSize}) or remove those
props and control sizing via the parent View/style (flex or explicit numeric
size computed with Dimensions) so the FaceId component gets numeric literals
rather than "100%".
- Around line 79-80: The two JSX View elements are using hardcoded gap={20};
replace those with the project design token for that spacing (e.g.,
gap={tokens.spacing.md} or gap={tokens.space[20]}) by importing the
tokens/spacing object from your design tokens module and using the appropriate
token value (ensure the token name matches the project's token API and types),
updating both occurrences of gap={20} on the View components.
- Around line 54-66: The code assumes providers.providers.manteca exists and
reads its status directly; add a defensive null/undefined check for
providers.providers.manteca (after the queryClient.fetchQuery/getRampProviders
call) before accessing manteca.status and set newStatus to a safe fallback
(e.g., "error" or "UNAVAILABLE") when manteca is missing, then call
router.replace accordingly (ensure logic around newStatus, router.replace, and
the existing branches for "ACTIVE"/"ONBOARDING" handle the fallback case).

In `@src/components/add-funds/Onboard.tsx`:
- Around line 26-96: The Onboard component contains routing, mutations and
server-state logic (useMutation with mutationFn, getKYCStatus handling,
completeOnboarding, queryClient calls, router.push/replace) and must be
refactored into a smart screen in src/app/**; extract all side-effect logic
(handleOnboarding mutation, completeOnboarding function,
queryClient.fetch/invalidate, getKYCStatus, startRampOnboarding, router
navigation and error handling) into a container/screen component and convert
this file into a pure presentational component that accepts props like currency,
validCurrency, CurrencyImage, isPending, onContinue (callback) and onNavigate
(or expose status callbacks). Keep only UI rendering and call props.onContinue
from handleContinue; remove any direct use of router, queryClient, useMutation,
and reportError from this component. Ensure function names referenced (Onboard,
completeOnboarding, handleOnboarding, handleContinue) are implemented/used in
the new smart screen and the UI component only receives data and callbacks via
props.
- Around line 34-57: The inline multi-line mutationFn passed to useMutation
(alias handleOnboarding) contains complex logic (calls to getKYCStatus,
router.push/replace and completeOnboarding) and should be extracted into a named
async function (e.g., async function performRampOnboarding(currency) { ... })
declared outside the inline prop so the handler is stable and testable; replace
mutationFn: async () => { ... } with mutationFn: performRampOnboarding (or () =>
performRampOnboarding(currency)) and move the existing logic (including the
APIError catch, kycCode calculation, router.push/replace and completeOnboarding
calls) into that function. Do the same refactor for the other multi-line
callbacks noted (the handler around completeOnboarding / status flow referenced
later) so they become named functions instead of inline multi-line arrow
functions.
- Around line 1-24: The React default import is only used for the React.FC type
in the currencyImages declaration; change the import to a type-only import and
update the type usage to use a type import (e.g., import type { FC } from
"react") and then type currencyImages as Record<string, FC<{ height: string;
width: string }>> so the runtime bundle does not include React unnecessarily;
update the top import line accordingly and remove the unused default React
import.
- Around line 100-123: Replace hardcoded numeric spacing and longhand padding
with Tamagui design tokens and shorthand props: change gap={20} and gap={10} to
token-based values (e.g. gap="$s6" or "$s2" as appropriate) on the View and
inner row, replace padding="$s4" longhand on YStack with the shorthand p="$s4",
and prefer shorthands like px, py, m, mt, etc., on components such as View,
ScrollView, YStack, Pressable and the container wrapping CurrencyImage; keep
existing behavior for router.back()/router.replace() and retain the
ArrowLeft/Pressable structure while only updating spacing props to use the
tokens from tamagui.config.ts.
- Around line 30-72: Move the top-level countryCode read into the
completeOnboarding function to avoid pinning an empty-string cache key; remove
the module-scope const countryCode and instead call
queryClient.getQueryData<string>(["user","country"]) (or similar) inside
completeOnboarding before you call queryClient.fetchQuery({ queryKey:
["ramp","providers", countryCode], ... }) so the providers query is only keyed
with the actual country value (and not a default ""), and apply the same change
to the analogous code in Kyc.tsx.

In `@src/components/add-funds/Ramp.tsx`:
- Around line 181-183: The X icon inside Button.Icon is missing an explicit size
prop and should match the other icons in this component for visual consistency;
update the X component in the Button.Icon block (the X element) to include the
same size prop used by the other icons in this file (e.g., size=16 or the
project's standard icon size) so all icons in the Ramp/Add-funds component
render at the same dimensions.
- Around line 90-91: Replace hardcoded numeric gaps in the Ramp component's JSX
(the View elements using gap={20}) with the app's design tokens: locate the View
elements in Ramp.tsx (props gap={20}) and change them to use the shared spacing
token (e.g., spacing.X or tokens.space.xxx — whatever the project's spacing
token export is) so all gap props reference the token constant; update both
occurrences mentioned and any other hardcoded gap values in this file to use the
same token import.
- Around line 107-109: The Info Pressable in Ramp.tsx is missing an onPress
handler, so either remove the Pressable or add an onPress that mirrors the
behavior used elsewhere (e.g., the help/article opener in AddFunds.tsx); locate
the Pressable wrapping the Info icon and implement an onPress prop that opens
the appropriate help modal or navigator route (or call the same helper function
used by AddFunds), and keep hitSlop={15} if retaining the button for consistent
touch target.
- Around line 104-106: Replace the hardcoded style props on the Text component
(currently fontSize={15} and fontWeight="bold") with the design-system token
props used elsewhere (e.g., use the Text variant/size prop like subHeadline and
the emphasis prop like emphasized or the component's weight token) so styling
comes from predefined tokens; update the Text invocation in the Ramp component
to remove fontSize/fontWeight and pass the appropriate predefined props
(subHeadline and emphasized) to match the design system.

In `@src/components/add-funds/Status.tsx`:
- Around line 43-45: The SVG components FaceId and Denied are being passed
percentage strings for width/height which SVG in React Native doesn't support;
update the JSX in the View that renders these icons (the conditional using
isOnboarding) to pass numeric width and height values (e.g., width={...}
height={...}) or compute numbers from the container/screen dimensions before
passing them; ensure the values are numeric literals or variables (not strings)
so FaceId and Denied receive valid numeric props.
- Around line 38-40: Replace the hardcoded gap={20} uses in the Status
component's View elements with the appropriate spacing design token from
tamagui.config.ts (e.g., the spacing token equivalent like "$4" or "space-4");
update both the outer View (the fullScreen padded container) and the inner View
(the child with gap) to use that token and, if required by your setup,
import/consume the token or theme so the component uses the predefined spacing
value rather than a raw number.

In `@src/utils/persona.ts`:
- Around line 117-205: startMantecaKYC duplicates large portions of startKYC
(pagehide listener, abort handling, Persona client/inquiry setup); extract
shared behavior into small helpers to remove duplication. Create a helper like
setupPagehideAbort(controller) that installs/removes the pagehide listener and
re-use it in both startMantecaKYC and startKYC; extract a common
attachAbortHandler(controller.signal, onAbort) pattern and a factory that wires
Persona callbacks (for Client and Inquiry) to shared onComplete/onCancel/onError
logic (referencing Client, Inquiry, onAbort, controller.signal,
queryClient.invalidateQueries and reportError) so both functions call the shared
helpers and only implement the KYC-specific return/resolve logic.

In `@src/utils/server.ts`:
- Around line 170-171: The error parsing uses a TypeScript type assertion
((await response.json()) as { code: string }) which bypasses runtime validation;
define a valibot schema (e.g., APIErrorResponse = object({ code: string() }))
and replace the assertions by calling parse(APIErrorResponse, await
response.json()) and destructuring the returned { code } before throwing
APIError; import object, string, and parse from valibot and apply this change
consistently for the locations that construct APIError (the occurrences around
the current response handling code and the other mentioned spots).
- Line 176: Remove the redundant type redeclaration that shadows the imported
RampProvider: delete the line exporting "export type RampProvider = (typeof
RampProvider)[number];" so the imported RampProvider from
`@exactly/server/utils/ramps/shared` is used; if you intended to also re-export a
runtime value, instead export the imported symbol (e.g., export { RampProvider
}) rather than redefining its type.

Comment on lines +10 to +13
type AddFiatButtonProperties = {
currency: string;
status: "ACTIVE" | "NOT_AVAILABLE" | "NOT_STARTED" | "ONBOARDING";
};
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using the Currency type for stronger typing.

The currency prop is typed as string, but the component uses currencyMap which has specific keys. Using the Currency type from ../../utils/currencies would provide better type safety.

♻️ Suggested improvement
+import { currencyMap, type Currency } from "../../utils/currencies";
-import { currencyMap } from "../../utils/currencies";

 type AddFiatButtonProperties = {
-  currency: string;
+  currency: Currency | string;
   status: "ACTIVE" | "NOT_AVAILABLE" | "NOT_STARTED" | "ONBOARDING";
 };
📝 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
type AddFiatButtonProperties = {
currency: string;
status: "ACTIVE" | "NOT_AVAILABLE" | "NOT_STARTED" | "ONBOARDING";
};
import { currencyMap, type Currency } from "../../utils/currencies";
type AddFiatButtonProperties = {
currency: Currency | string;
status: "ACTIVE" | "NOT_AVAILABLE" | "NOT_STARTED" | "ONBOARDING";
};
🤖 Prompt for AI Agents
In `@src/components/add-funds/AddFiatButton.tsx` around lines 10 - 13, The
AddFiatButtonProperties type declares currency as a plain string which weakens
type safety against currencyMap keys; import the Currency type from
../../utils/currencies and change the property signature in
AddFiatButtonProperties from currency: string to currency: Currency so the
component's currency prop is strongly typed to the known currency keys (update
any usages of AddFiatButtonProperties/AddFiatButton to satisfy the new type if
necessary).

Comment on lines +107 to +111
{countryCode && isPending ? (
<View justifyContent="center" alignItems="center">
<Skeleton width="100%" height={82} />
</View>
) : (
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider showing skeleton when countryCode is undefined and loading.

The current condition countryCode && isPending only shows the skeleton when countryCode exists and providers are loading. If countryCode is still being fetched, no loading indicator appears for the fiat options section.

♻️ Alternative condition
-            {countryCode && isPending ? (
+            {isPending || !providers ? (

This shows the skeleton while either the country or providers are loading, providing better UX feedback.

🤖 Prompt for AI Agents
In `@src/components/add-funds/AddFunds.tsx` around lines 107 - 111, In the
AddFunds component JSX where the fiat options are rendered (the block using
countryCode and isPending to decide between the Skeleton and options), change
the condition from countryCode && isPending to show the skeleton when either the
providers are loading or the country code is not yet available (e.g., use
isPending || !countryCode or !countryCode || isPending) so the loading skeleton
is displayed while countryCode is being fetched as well as while providers are
loading.

</View>
) : (
providers && (
<YStack gap={16}>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use design tokens for gap values.

gap={16} should use a design token instead of a hardcoded number.

As per coding guidelines: "All styling must use predefined design tokens."

♻️ Suggested fix
-                  <YStack gap={16}>
+                  <YStack gap="$s4">
📝 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
<YStack gap={16}>
<YStack gap="$s4">
🤖 Prompt for AI Agents
In `@src/components/add-funds/AddFunds.tsx` at line 113, In AddFunds.tsx replace
the hardcoded gap={16} on the YStack with the appropriate design token (e.g.,
the app's spacing token such as spacing.medium or $4) used across the codebase;
update the YStack prop to use that token (and import the token if needed) so
styling relies on predefined design tokens rather than a raw number, then run
the linter/formatter to ensure the prop type matches the token format.

Comment on lines +54 to +66
const providers = await queryClient.fetchQuery({
queryKey: ["ramp", "providers", countryCode],
queryFn: () => getRampProviders(countryCode),
staleTime: 0,
});
const newStatus = providers.providers.manteca.status;
if (newStatus === "ACTIVE") {
router.replace({ pathname: "/add-funds/ramp", params: { currency } });
} else if (newStatus === "ONBOARDING") {
router.replace({ pathname: "/add-funds/status", params: { status: newStatus, currency } });
} else {
router.replace({ pathname: "/add-funds/status", params: { status: "error", currency } });
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add null check for manteca provider to prevent runtime error.

providers.providers.manteca is accessed without checking if the manteca key exists. If the provider response doesn't include manteca (e.g., due to country restrictions), this will throw a runtime error.

🛡️ Add defensive check
       const providers = await queryClient.fetchQuery({
         queryKey: ["ramp", "providers", countryCode],
         queryFn: () => getRampProviders(countryCode),
         staleTime: 0,
       });
-      const newStatus = providers.providers.manteca.status;
+      const mantecaProvider = providers.providers.manteca;
+      if (!mantecaProvider) {
+        router.replace({ pathname: "/add-funds/status", params: { status: "error", currency } });
+        return;
+      }
+      const newStatus = mantecaProvider.status;
       if (newStatus === "ACTIVE") {
📝 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
const providers = await queryClient.fetchQuery({
queryKey: ["ramp", "providers", countryCode],
queryFn: () => getRampProviders(countryCode),
staleTime: 0,
});
const newStatus = providers.providers.manteca.status;
if (newStatus === "ACTIVE") {
router.replace({ pathname: "/add-funds/ramp", params: { currency } });
} else if (newStatus === "ONBOARDING") {
router.replace({ pathname: "/add-funds/status", params: { status: newStatus, currency } });
} else {
router.replace({ pathname: "/add-funds/status", params: { status: "error", currency } });
}
const providers = await queryClient.fetchQuery({
queryKey: ["ramp", "providers", countryCode],
queryFn: () => getRampProviders(countryCode),
staleTime: 0,
});
const mantecaProvider = providers.providers.manteca;
if (!mantecaProvider) {
router.replace({ pathname: "/add-funds/status", params: { status: "error", currency } });
return;
}
const newStatus = mantecaProvider.status;
if (newStatus === "ACTIVE") {
router.replace({ pathname: "/add-funds/ramp", params: { currency } });
} else if (newStatus === "ONBOARDING") {
router.replace({ pathname: "/add-funds/status", params: { status: newStatus, currency } });
} else {
router.replace({ pathname: "/add-funds/status", params: { status: "error", currency } });
}
🤖 Prompt for AI Agents
In `@src/components/add-funds/Kyc.tsx` around lines 54 - 66, The code assumes
providers.providers.manteca exists and reads its status directly; add a
defensive null/undefined check for providers.providers.manteca (after the
queryClient.fetchQuery/getRampProviders call) before accessing manteca.status
and set newStatus to a safe fallback (e.g., "error" or "UNAVAILABLE") when
manteca is missing, then call router.replace accordingly (ensure logic around
newStatus, router.replace, and the existing branches for "ACTIVE"/"ONBOARDING"
handle the fallback case).

Comment on lines +79 to +80
<View gap={20} fullScreen padded>
<View gap={20}>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use design tokens for gap values.

Hardcoded gap={20} values should use design tokens.

As per coding guidelines: "All styling must use predefined design tokens."

♻️ Suggested fix
-      <View gap={20} fullScreen padded>
-        <View gap={20}>
+      <View gap="$s5" fullScreen padded>
+        <View gap="$s5">
📝 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
<View gap={20} fullScreen padded>
<View gap={20}>
<View gap="$s5" fullScreen padded>
<View gap="$s5">
🤖 Prompt for AI Agents
In `@src/components/add-funds/Kyc.tsx` around lines 79 - 80, The two JSX View
elements are using hardcoded gap={20}; replace those with the project design
token for that spacing (e.g., gap={tokens.spacing.md} or gap={tokens.space[20]})
by importing the tokens/spacing object from your design tokens module and using
the appropriate token value (ensure the token name matches the project's token
API and types), updating both occurrences of gap={20} on the View components.

Comment on lines +38 to +40
<View gap={20} fullScreen padded>
<ScrollView flex={1}>
<View flex={1} gap={20}>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use design tokens for gap values.

The gap={20} values should use predefined design tokens from tamagui.config.ts instead of hardcoded numbers.

As per coding guidelines: "All styling must use predefined design tokens from tamagui.config.ts. Never use hardcoded values for colors, spacing, fonts, etc."

♻️ Suggested fix
-      <View gap={20} fullScreen padded>
-        <ScrollView flex={1}>
-          <View flex={1} gap={20}>
+      <View gap="$s5" fullScreen padded>
+        <ScrollView flex={1}>
+          <View flex={1} gap="$s5">
📝 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
<View gap={20} fullScreen padded>
<ScrollView flex={1}>
<View flex={1} gap={20}>
<View gap="$s5" fullScreen padded>
<ScrollView flex={1}>
<View flex={1} gap="$s5">
🤖 Prompt for AI Agents
In `@src/components/add-funds/Status.tsx` around lines 38 - 40, Replace the
hardcoded gap={20} uses in the Status component's View elements with the
appropriate spacing design token from tamagui.config.ts (e.g., the spacing token
equivalent like "$4" or "space-4"); update both the outer View (the fullScreen
padded container) and the inner View (the child with gap) to use that token and,
if required by your setup, import/consume the token or theme so the component
uses the predefined spacing value rather than a raw number.

Comment on lines +43 to +45
<View width="100%" aspectRatio={1} justifyContent="center" alignItems="center">
{isOnboarding ? <FaceId width="100%" height="100%" /> : <Denied width="100%" height="100%" />}
</View>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SVG components require numeric dimensions, not string percentages.

Using width="100%" and height="100%" on SVG components won't work as expected in React Native. SVG props don't resolve percentage strings. Pass numeric values or use the parent container's dimensions.

Based on learnings: "In React Native using SVG components, pass numeric literals for width and height (width={32}, height={32}) rather than design token strings."

🛠️ Suggested fix
               <View width="100%" aspectRatio={1} justifyContent="center" alignItems="center">
-                  {isOnboarding ? <FaceId width="100%" height="100%" /> : <Denied width="100%" height="100%" />}
+                  {isOnboarding ? <FaceId width={200} height={200} /> : <Denied width={200} height={200} />}
               </View>

Alternatively, compute dimensions dynamically from the container or screen dimensions.

📝 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
<View width="100%" aspectRatio={1} justifyContent="center" alignItems="center">
{isOnboarding ? <FaceId width="100%" height="100%" /> : <Denied width="100%" height="100%" />}
</View>
<View width="100%" aspectRatio={1} justifyContent="center" alignItems="center">
{isOnboarding ? <FaceId width={200} height={200} /> : <Denied width={200} height={200} />}
</View>
🤖 Prompt for AI Agents
In `@src/components/add-funds/Status.tsx` around lines 43 - 45, The SVG components
FaceId and Denied are being passed percentage strings for width/height which SVG
in React Native doesn't support; update the JSX in the View that renders these
icons (the conditional using isOnboarding) to pass numeric width and height
values (e.g., width={...} height={...}) or compute numbers from the
container/screen dimensions before passing them; ensure the values are numeric
literals or variables (not strings) so FaceId and Denied receive valid numeric
props.

Comment on lines +117 to +205
export function startMantecaKYC() {
if (current && !current.controller.signal.aborted && current.type === "manteca") return current.promise;

current?.controller.abort(new Error("persona inquiry aborted"));
const controller = new AbortController();

const promise = (async () => {
const { signal } = controller;

if (Platform.OS === "web") {
const onPageHide = () => controller.abort(new Error("page unloaded"));
globalThis.addEventListener("pagehide", onPageHide);
signal.addEventListener("abort", () => globalThis.removeEventListener("pagehide", onPageHide), { once: true });
}

if (Platform.OS === "web") {
const [{ Client }, { inquiryId, sessionToken }] = await Promise.all([
import("persona"),
getKYCTokens("manteca", await getRedirectURI()),
]);
if (signal.aborted) throw signal.reason;

return new Promise<MantecaKYCResult>((resolve, reject) => {
const onAbort = () => {
client.destroy();
reject(new Error("persona inquiry aborted", { cause: signal.reason }));
};
const client = new Client({
inquiryId,
sessionToken,
environment: environment as "production" | "sandbox",
onReady: () => client.open(),
onComplete: () => {
signal.removeEventListener("abort", onAbort);
client.destroy();
queryClient.invalidateQueries({ queryKey: ["kyc", "manteca"] }).catch(reportError);
resolve({ status: "complete" });
},
onCancel: () => {
signal.removeEventListener("abort", onAbort);
client.destroy();
queryClient.invalidateQueries({ queryKey: ["kyc", "manteca"] }).catch(reportError);
resolve({ status: "cancel" });
},
onError: (error) => {
signal.removeEventListener("abort", onAbort);
client.destroy();
reportError(error);
resolve({ status: "error" });
},
});
signal.addEventListener("abort", onAbort, { once: true });
});
}

const { inquiryId, sessionToken } = await getKYCTokens("manteca", await getRedirectURI());
if (signal.aborted) throw signal.reason;

const { Inquiry } = await import("react-native-persona");
return new Promise<MantecaKYCResult>((resolve, reject) => {
const onAbort = () => reject(new Error("persona inquiry aborted", { cause: signal.reason }));
signal.addEventListener("abort", onAbort, { once: true });
Inquiry.fromInquiry(inquiryId)
.sessionToken(sessionToken)
.onCanceled(() => {
signal.removeEventListener("abort", onAbort);
queryClient.invalidateQueries({ queryKey: ["kyc", "manteca"] }).catch(reportError);
resolve({ status: "cancel" });
})
.onComplete(() => {
signal.removeEventListener("abort", onAbort);
queryClient.invalidateQueries({ queryKey: ["kyc", "manteca"] }).catch(reportError);
resolve({ status: "complete" });
})
.onError((error) => {
signal.removeEventListener("abort", onAbort);
reportError(error);
resolve({ status: "error" });
})
.build()
.start();
});
})().finally(() => {
if (current?.controller === controller) current = undefined;
});

current = { type: "manteca", controller, promise };
return promise;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting shared logic to reduce duplication.

startMantecaKYC duplicates ~80 lines from startKYC. While the return types and post-completion behavior differ intentionally, the following patterns are identical and could be extracted:

  1. Pagehide listener setup (lines 126-130 mirror 33-36)
  2. Abort signal handling pattern
  3. Persona Client/Inquiry builder configuration

This would make future maintenance easier and reduce the risk of divergence bugs.

♻️ Example helper extraction
// helper for pagehide cleanup
function setupPagehideAbort(controller: AbortController) {
  if (Platform.OS !== "web") return;
  const onPageHide = () => controller.abort(new Error("page unloaded"));
  globalThis.addEventListener("pagehide", onPageHide);
  controller.signal.addEventListener(
    "abort",
    () => globalThis.removeEventListener("pagehide", onPageHide),
    { once: true }
  );
}

Similar helpers could wrap the Persona Client/Inquiry setup with callbacks for onComplete/onCancel/onError.

🤖 Prompt for AI Agents
In `@src/utils/persona.ts` around lines 117 - 205, startMantecaKYC duplicates
large portions of startKYC (pagehide listener, abort handling, Persona
client/inquiry setup); extract shared behavior into small helpers to remove
duplication. Create a helper like setupPagehideAbort(controller) that
installs/removes the pagehide listener and re-use it in both startMantecaKYC and
startKYC; extract a common attachAbortHandler(controller.signal, onAbort)
pattern and a factory that wires Persona callbacks (for Client and Inquiry) to
shared onComplete/onCancel/onError logic (referencing Client, Inquiry, onAbort,
controller.signal, queryClient.invalidateQueries and reportError) so both
functions call the shared helpers and only implement the KYC-specific
return/resolve logic.

Comment on lines +170 to +171
const { code } = (await response.json()) as { code: string };
throw new APIError(response.status, code);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Type assertion bypasses runtime validation.

Using as { code: string } bypasses valibot's runtime validation. Consider defining a schema for API error responses and parsing with it for consistent error handling.

♻️ Proposed pattern
import { object, string, parse } from "valibot";

const APIErrorResponse = object({ code: string() });

// Then in error handling:
const { code } = parse(APIErrorResponse, await response.json());

This pattern could be applied consistently across lines 170, 186, 283, 295, and 309.

🤖 Prompt for AI Agents
In `@src/utils/server.ts` around lines 170 - 171, The error parsing uses a
TypeScript type assertion ((await response.json()) as { code: string }) which
bypasses runtime validation; define a valibot schema (e.g., APIErrorResponse =
object({ code: string() })) and replace the assertions by calling
parse(APIErrorResponse, await response.json()) and destructuring the returned {
code } before throwing APIError; import object, string, and parse from valibot
and apply this change consistently for the locations that construct APIError
(the occurrences around the current response handling code and the other
mentioned spots).

return response.json();
}

export type RampProvider = (typeof RampProvider)[number];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove redundant type redeclaration.

RampProvider is already imported from @exactly/server/utils/ramps/shared on line 20. This line redeclares it, which shadows the import and triggers the static analysis error.

🐛 Proposed fix
-export type RampProvider = (typeof RampProvider)[number];
+export type { RampProvider };

Or if you need to re-export the runtime value as well:

+export { RampProvider };
🧰 Tools
🪛 Biome (2.3.13)

[error] 176-176: Shouldn't redeclare 'RampProvider'. Consider to delete it or rename it.

'RampProvider' is defined here:

(lint/suspicious/noRedeclare)

🤖 Prompt for AI Agents
In `@src/utils/server.ts` at line 176, Remove the redundant type redeclaration
that shadows the imported RampProvider: delete the line exporting "export type
RampProvider = (typeof RampProvider)[number];" so the imported RampProvider from
`@exactly/server/utils/ramps/shared` is used; if you intended to also re-export a
runtime value, instead export the imported symbol (e.g., export { RampProvider
}) rather than redefining its type.

Copy link

@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: 6

Caution

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

⚠️ Outside diff range comments (1)
src/components/add-funds/AddFunds.tsx (1)

57-58: 🧹 Nitpick | 🔵 Trivial

Use design tokens for gap values.

gap={20} should use a design token instead of a hardcoded number.

As per coding guidelines: "All styling must use predefined design tokens."

♻️ Suggested fix
-        <View gap={20} fullScreen padded>
-          <YStack gap={20}>
+        <View gap="$s5" fullScreen padded>
+          <YStack gap="$s5">
🤖 Fix all issues with AI agents
In `@src/components/add-funds/AddFunds.tsx`:
- Around line 38-46: The current useQuery implementation relies on getKYCStatus
as a side-effect to populate queryClient and then reads back via
queryClient.getQueryData, which is indirect and returns "" on failure (truthy)
so downstream providers query can enable incorrectly; change the query to return
the country code directly (have queryFn call an API that returns country or call
getKYCStatus and return its country value) or use the useQuery `select` option
to extract the country from the getKYCStatus response instead of reading
queryClient; also ensure the queryFn returns undefined (not empty string) when
the country is absent or getKYCStatus throws so the providers query remains
disabled.

In `@src/components/add-funds/Kyc.tsx`:
- Around line 23-71: The Kyc component currently contains routing, mutations and
cache reads (useMutation handling via handleContinue, completeOnboarding,
startMantecaKYC, startRampOnboarding, queryClient and router usage) and must be
converted to a dumb presentational component; extract all side-effect and
stateful logic into a new screen/container (e.g., KycContainer) which owns the
mutation (mutationKey ["kyc","complete","manteca"]), completeOnboarding flow,
queryClient interactions (invalidateQueries/fetchQuery), router.replace calls
and error reporting (reportError), and pass only props into the Kyc UI component
(e.g., isPending, onContinue callback, validCurrency, currency, countryCode) so
src/components/Kyc.tsx becomes purely presentational and receives callbacks/data
via props.
- Around line 30-43: The inline multi-line mutationFn passed to useMutation and
the multi-line Pressable onPress handler should be moved into named function
declarations for stability and readability: extract the async mutation body into
a function like function handleMantecaKyc() that calls startMantecaKYC(), checks
result.status ("cancel" / "error") and calls router.replace or
completeOnboarding() as currently implemented, then pass that function as
mutationFn to useMutation; likewise extract the Pressable handler into a named
function (e.g., function onPressContinue()) and reference it in the Pressable.
Ensure you keep existing symbols (startMantecaKYC, completeOnboarding,
router.replace, mutateAsync/handleContinue) and behavior unchanged while
replacing inline arrow bodies with these function declarations.

In `@src/components/add-funds/Onboard.tsx`:
- Line 24: The currencyImages map currently types SVG components as React.FC<{
height: string; width: string }> and the ARS, BRL, USD components are being
passed string percentages like "100%"; change the prop type to React.FC<{
height: number; width: number }>, update any usages of
currencyImages/ARS/BRL/USD to compute and pass numeric pixel values (e.g.,
containerHeight or parsed values) instead of "100%", and adjust places
referenced around the same usage (lines ~120-121) to pass numbers so the SVG
components receive numeric dimensions.

In `@src/components/add-funds/Ramp.tsx`:
- Line 92: Replace the hardcoded numeric gap value on the JSX View inside the
Ramp component: locate the View element with props flexDirection="row" gap={10}
justifyContent="space-between" alignItems="center" and change gap to use the
design token (for example gap="$s3") so all styling uses predefined design
tokens instead of raw numbers.

In `@src/utils/currencies.ts`:
- Around line 1-12: Replace the manual Currency type and import with valibot's
InferInput: import InferInput from valibot and derive Currency from the already
defined CurrencySchema (e.g., type Currency = InferInput<typeof CurrencySchema>)
instead of using keyof typeof currencyMap; keep currencyMap and CurrencySchema
(picklist) as-is so the schema is the single source of truth and the derived
Currency type stays in sync with CurrencySchema and currencyMap.

Comment on lines +38 to +46
const { data: countryCode } = useQuery({
queryKey: ["user", "country"],
queryFn: async () => {
await getKYCStatus("basic", true);
return queryClient.getQueryData<string>(["user", "country"]) ?? "";
},
staleTime: (query) => (query.state.data ? Infinity : 0),
retry: false,
});
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

The query pattern relies on side-effect data population.

The queryFn calls getKYCStatus which sets ["user", "country"] in the query client as a side effect, then reads it back. This works but is indirect. Consider returning the country code directly from the API or using select to extract it from the response.

Additionally, if getKYCStatus throws before setting the query data, the fallback empty string "" will be returned, which is truthy and may enable the providers query unexpectedly.

♻️ Potential simplification
   const { data: countryCode } = useQuery({
     queryKey: ["user", "country"],
     queryFn: async () => {
       await getKYCStatus("basic", true);
-      return queryClient.getQueryData<string>(["user", "country"]) ?? "";
+      return queryClient.getQueryData<string>(["user", "country"]);
     },
     staleTime: (query) => (query.state.data ? Infinity : 0),
     retry: false,
   });

This returns undefined instead of "" on failure, making the providers query stay disabled.

📝 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
const { data: countryCode } = useQuery({
queryKey: ["user", "country"],
queryFn: async () => {
await getKYCStatus("basic", true);
return queryClient.getQueryData<string>(["user", "country"]) ?? "";
},
staleTime: (query) => (query.state.data ? Infinity : 0),
retry: false,
});
const { data: countryCode } = useQuery({
queryKey: ["user", "country"],
queryFn: async () => {
await getKYCStatus("basic", true);
return queryClient.getQueryData<string>(["user", "country"]);
},
staleTime: (query) => (query.state.data ? Infinity : 0),
retry: false,
});
🤖 Prompt for AI Agents
In `@src/components/add-funds/AddFunds.tsx` around lines 38 - 46, The current
useQuery implementation relies on getKYCStatus as a side-effect to populate
queryClient and then reads back via queryClient.getQueryData, which is indirect
and returns "" on failure (truthy) so downstream providers query can enable
incorrectly; change the query to return the country code directly (have queryFn
call an API that returns country or call getKYCStatus and return its country
value) or use the useQuery `select` option to extract the country from the
getKYCStatus response instead of reading queryClient; also ensure the queryFn
returns undefined (not empty string) when the country is absent or getKYCStatus
throws so the providers query remains disabled.

Comment on lines +23 to +71
export default function Kyc() {
const { t } = useTranslation();
const router = useRouter();
const { currency } = useLocalSearchParams<{ currency: string }>();
const countryCode = queryClient.getQueryData<string>(["user", "country"]) ?? "";
const validCurrency = isValidCurrency(currency);

const { mutateAsync: handleContinue, isPending } = useMutation({
mutationKey: ["kyc", "complete", "manteca"],
mutationFn: async () => {
const result = await startMantecaKYC();
if (result.status === "cancel") return;
if (result.status === "error") {
router.replace({
pathname: "/add-funds/status",
params: { status: "error", currency },
});
return;
}
await completeOnboarding();
},
});

if (!validCurrency) return <Redirect href="/add-funds" />;

async function completeOnboarding() {
try {
await startRampOnboarding({ provider: "manteca" });

await queryClient.invalidateQueries({ queryKey: ["ramp", "providers"] });

const providers = await queryClient.fetchQuery({
queryKey: ["ramp", "providers", countryCode],
queryFn: () => getRampProviders(countryCode),
staleTime: 0,
});
const newStatus = providers.providers.manteca.status;
if (newStatus === "ACTIVE") {
router.replace({ pathname: "/add-funds/ramp", params: { currency } });
} else if (newStatus === "ONBOARDING") {
router.replace({ pathname: "/add-funds/status", params: { status: newStatus, currency } });
} else {
router.replace({ pathname: "/add-funds/status", params: { status: "error", currency } });
}
} catch (error) {
reportError(error);
router.replace({ pathname: "/add-funds/status", params: { status: "error", currency } });
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Move Kyc screen logic out of src/components.

This component drives routing, mutations, and cache reads; per project structure rules it should be a screen/container, leaving this file as a presentational UI component.
As per coding guidelines: "src/components/**/*.{ts,tsx}: Dumb components (UI) should receive data and callbacks via props with no knowledge of the application's state."

🤖 Prompt for AI Agents
In `@src/components/add-funds/Kyc.tsx` around lines 23 - 71, The Kyc component
currently contains routing, mutations and cache reads (useMutation handling via
handleContinue, completeOnboarding, startMantecaKYC, startRampOnboarding,
queryClient and router usage) and must be converted to a dumb presentational
component; extract all side-effect and stateful logic into a new
screen/container (e.g., KycContainer) which owns the mutation (mutationKey
["kyc","complete","manteca"]), completeOnboarding flow, queryClient interactions
(invalidateQueries/fetchQuery), router.replace calls and error reporting
(reportError), and pass only props into the Kyc UI component (e.g., isPending,
onContinue callback, validCurrency, currency, countryCode) so
src/components/Kyc.tsx becomes purely presentational and receives callbacks/data
via props.

Comment on lines +30 to +43
const { mutateAsync: handleContinue, isPending } = useMutation({
mutationKey: ["kyc", "complete", "manteca"],
mutationFn: async () => {
const result = await startMantecaKYC();
if (result.status === "cancel") return;
if (result.status === "error") {
router.replace({
pathname: "/add-funds/status",
params: { status: "error", currency },
});
return;
}
await completeOnboarding();
},
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Extract multi-line inline callbacks into named functions.

The mutationFn and Pressable handler are multi-line; move them to named functions for clarity and to keep handlers stable.
As per coding guidelines: "Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations."

♻️ Proposed refactor
-  const { mutateAsync: handleContinue, isPending } = useMutation({
-    mutationKey: ["kyc", "complete", "manteca"],
-    mutationFn: async () => {
-      const result = await startMantecaKYC();
-      if (result.status === "cancel") return;
-      if (result.status === "error") {
-        router.replace({
-          pathname: "/add-funds/status",
-          params: { status: "error", currency },
-        });
-        return;
-      }
-      await completeOnboarding();
-    },
-  });
+  async function runKycFlow() {
+    const result = await startMantecaKYC();
+    if (result.status === "cancel") return;
+    if (result.status === "error") {
+      router.replace({ pathname: "/add-funds/status", params: { status: "error", currency } });
+      return;
+    }
+    await completeOnboarding();
+  }
+
+  const { mutateAsync: handleContinue, isPending } = useMutation({
+    mutationKey: ["kyc", "complete", "manteca"],
+    mutationFn: runKycFlow,
+  });
+
+  function handleBackPress() {
+    if (router.canGoBack()) {
+      router.back();
+    } else {
+      router.replace("/(main)/(home)");
+    }
+  }
@@
-            <Pressable
-              onPress={() => {
-                if (router.canGoBack()) {
-                  router.back();
-                } else {
-                  router.replace("/(main)/(home)");
-                }
-              }}
-            >
+            <Pressable onPress={handleBackPress}>

Also applies to: 82-89

🤖 Prompt for AI Agents
In `@src/components/add-funds/Kyc.tsx` around lines 30 - 43, The inline multi-line
mutationFn passed to useMutation and the multi-line Pressable onPress handler
should be moved into named function declarations for stability and readability:
extract the async mutation body into a function like function handleMantecaKyc()
that calls startMantecaKYC(), checks result.status ("cancel" / "error") and
calls router.replace or completeOnboarding() as currently implemented, then pass
that function as mutationFn to useMutation; likewise extract the Pressable
handler into a named function (e.g., function onPressContinue()) and reference
it in the Pressable. Ensure you keep existing symbols (startMantecaKYC,
completeOnboarding, router.replace, mutateAsync/handleContinue) and behavior
unchanged while replacing inline arrow bodies with these function declarations.

import Text from "../shared/Text";
import View from "../shared/View";

const currencyImages: Record<string, React.FC<{ height: string; width: string }>> = { ARS, BRL, USD };
Copy link

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:

head -150 src/components/add-funds/Onboard.tsx | tail -130

Repository: exactly/exa

Length of output: 4870


🏁 Script executed:

sed -n '1,30p' src/components/add-funds/Onboard.tsx

Repository: exactly/exa

Length of output: 1272


🏁 Script executed:

sed -n '115,125p' src/components/add-funds/Onboard.tsx

Repository: exactly/exa

Length of output: 665


🏁 Script executed:

rg -n "width.*height" src/components/add-funds/Onboard.tsx

Repository: exactly/exa

Length of output: 121


🏁 Script executed:

fd -e svg src/assets/images | grep -E "(ars|brl|usd)" | head -5

Repository: exactly/exa

Length of output: 428


🏁 Script executed:

rg "CurrencyImage|ARS|BRL|USD" src/components --type tsx -A 2 | head -40

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

fd . src/assets/images | head -10

Repository: exactly/exa

Length of output: 407


🏁 Script executed:

rg "width=" src/components -A 2 | grep -E "(CurrencyImage|SVG|svg)" -A 2 -B 2 | head -30

Repository: exactly/exa

Length of output: 819


🏁 Script executed:

cat package.json | grep -E "(svg|react-native|expo)"

Repository: exactly/exa

Length of output: 3534


🏁 Script executed:

cat src/components/shared/Blocky.tsx | head -50

Repository: exactly/exa

Length of output: 1383


🏁 Script executed:

rg "React.FC.*height.*width" src/components -A 2 -B 2

Repository: exactly/exa

Length of output: 415


🏁 Script executed:

rg "SVG.*width|SVG.*height" src/components -B 2 -A 2 | head -40

Repository: exactly/exa

Length of output: 413


🏁 Script executed:

rg "currencyImages|ARS|BRL|USD" src/ -B 3 -A 3 --type ts --type tsx

Repository: exactly/exa

Length of output: 82


🏁 Script executed:

cat src/components/shared/Blocky.tsx | sed -n '1,10p'

Repository: exactly/exa

Length of output: 240


🏁 Script executed:

rg "currencyImages|ARS|BRL|USD" src -B 3 -A 3

Repository: exactly/exa

Length of output: 50367


🏁 Script executed:

cat src/components/shared/View.tsx | head -40

Repository: exactly/exa

Length of output: 516


Use numeric values for SVG component dimensions.

The currency image components (ARS, BRL, USD) are SVG-based and expect numeric dimensions, not string percentages. Update the prop type from string to number and pass computed numeric values instead of "100%".

🛠️ Example fix
-const currencyImages: Record<string, React.FC<{ height: string; width: string }>> = { ARS, BRL, USD };
+const currencyImages: Record<string, React.FC<{ height: number; width: number }>> = { ARS, BRL, USD };
@@
-                  <CurrencyImage width="100%" height="100%" />
+                  <CurrencyImage width={200} height={200} />

Also applies to: 120-121

🤖 Prompt for AI Agents
In `@src/components/add-funds/Onboard.tsx` at line 24, The currencyImages map
currently types SVG components as React.FC<{ height: string; width: string }>
and the ARS, BRL, USD components are being passed string percentages like
"100%"; change the prop type to React.FC<{ height: number; width: number }>,
update any usages of currencyImages/ARS/BRL/USD to compute and pass numeric
pixel values (e.g., containerHeight or parsed values) instead of "100%", and
adjust places referenced around the same usage (lines ~120-121) to pass numbers
so the SVG components receive numeric dimensions.

<SafeView fullScreen>
<View gap={20} fullScreen padded>
<View gap={20}>
<View flexDirection="row" gap={10} justifyContent="space-between" alignItems="center">
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use design tokens for gap values.

gap={10} should use a design token (e.g., gap="$s3").

As per coding guidelines: "All styling must use predefined design tokens."

♻️ Suggested fix
-          <View flexDirection="row" gap={10} justifyContent="space-between" alignItems="center">
+          <View flexDirection="row" gap="$s3" justifyContent="space-between" alignItems="center">
📝 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
<View flexDirection="row" gap={10} justifyContent="space-between" alignItems="center">
<View flexDirection="row" gap="$s3" justifyContent="space-between" alignItems="center">
🤖 Prompt for AI Agents
In `@src/components/add-funds/Ramp.tsx` at line 92, Replace the hardcoded numeric
gap value on the JSX View inside the Ramp component: locate the View element
with props flexDirection="row" gap={10} justifyContent="space-between"
alignItems="center" and change gap to use the design token (for example
gap="$s3") so all styling uses predefined design tokens instead of raw numbers.

Comment on lines +1 to +12
import { picklist, safeParse } from "valibot";

export const currencyMap = {
ARS: { name: "Argentinian Pesos", emoji: "🇦🇷" },
BRL: { name: "Brazilian Real", emoji: "🇧🇷" },
USD: { name: "US Dollars", emoji: "🇺🇸" },
} as const;

export type Currency = keyof typeof currencyMap;

export const CurrencySchema = picklist(Object.keys(currencyMap) as [Currency, ...Currency[]]);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if valibot is available and what it exports
rg -t ts "from ['\"]valibot" src/ | head -20

Repository: exactly/exa

Length of output: 1423


🏁 Script executed:

# Check the full context of the currencies.ts file
cat -n src/utils/currencies.ts

Repository: exactly/exa

Length of output: 666


🏁 Script executed:

# Search for usages of Currency type to understand if changing its definition would impact anything
rg "Currency" src/ -t ts --context 2

Repository: exactly/exa

Length of output: 5882


🏁 Script executed:

# Check if valibot's Input type is documented in package.json or dependencies
cat package.json | grep -A 5 "valibot"

Repository: exactly/exa

Length of output: 419


🏁 Script executed:

# Check if there are other files using Input from valibot to confirm the correct export name
rg "Input|InferInput" src/ -t ts | grep "from.*valibot"

Repository: exactly/exa

Length of output: 167


🏁 Script executed:

# Verify the valibot version and API
cat package.json | grep -E '"valibot":|"@hono/valibot-validator":'

Repository: exactly/exa

Length of output: 81


🌐 Web query:

valibot Input type export TypeScript inference

💡 Result:

Use Valibot’s utility type InferInput and export it from the schema’s typeof:

import * as v from "valibot";

export const LoginSchema = v.object({
  email: v.string(),
  password: v.string(),
});

export type LoginInput = v.InferInput<typeof LoginSchema>;
export type LoginOutput = v.InferOutput<typeof LoginSchema>; // optional
  • InferInput<typeof Schema> = the type accepted as input to validation. (valibot.dev)
  • InferOutput<typeof Schema> = the type returned by parse/safeParse (can differ if you use defaults/transforms/brands, etc.). (valibot.dev)
  • If you want a clean export pattern for both, Valibot recommends suffixing with Schema/Input/Output. (valibot.dev)

Citations:


Use InferInput instead of Input to derive Currency from CurrencySchema.

The core concept is correct—derive the type from the schema to keep them in sync and avoid the circular reference. However, valibot uses InferInput, not Input. The codebase already uses this pattern in src/utils/server.ts.

♻️ Suggested fix
-import { picklist, safeParse } from "valibot";
+import { picklist, safeParse } from "valibot";
+import type { InferInput } from "valibot";
@@
-export type Currency = keyof typeof currencyMap;
-
-export const CurrencySchema = picklist(Object.keys(currencyMap) as [Currency, ...Currency[]]);
+export const CurrencySchema = picklist(
+  Object.keys(currencyMap) as [keyof typeof currencyMap, ...(keyof typeof currencyMap)[]],
+);
+export type Currency = InferInput<typeof CurrencySchema>;
📝 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
import { picklist, safeParse } from "valibot";
export const currencyMap = {
ARS: { name: "Argentinian Pesos", emoji: "🇦🇷" },
BRL: { name: "Brazilian Real", emoji: "🇧🇷" },
USD: { name: "US Dollars", emoji: "🇺🇸" },
} as const;
export type Currency = keyof typeof currencyMap;
export const CurrencySchema = picklist(Object.keys(currencyMap) as [Currency, ...Currency[]]);
import { picklist, safeParse } from "valibot";
import type { InferInput } from "valibot";
export const currencyMap = {
ARS: { name: "Argentinian Pesos", emoji: "🇦🇷" },
BRL: { name: "Brazilian Real", emoji: "🇧🇷" },
USD: { name: "US Dollars", emoji: "🇺🇸" },
} as const;
export const CurrencySchema = picklist(
Object.keys(currencyMap) as [keyof typeof currencyMap, ...(keyof typeof currencyMap)[]],
);
export type Currency = InferInput<typeof CurrencySchema>;
🤖 Prompt for AI Agents
In `@src/utils/currencies.ts` around lines 1 - 12, Replace the manual Currency
type and import with valibot's InferInput: import InferInput from valibot and
derive Currency from the already defined CurrencySchema (e.g., type Currency =
InferInput<typeof CurrencySchema>) instead of using keyof typeof currencyMap;
keep currencyMap and CurrencySchema (picklist) as-is so the schema is the single
source of truth and the derived Currency type stays in sync with CurrencySchema
and currencyMap.

Copy link

@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: 5

🤖 Fix all issues with AI agents
In `@src/components/add-funds/AddFunds.tsx`:
- Around line 93-95: The onPress handler currently calls router.push with a raw
string ("/add-funds/bridge"); replace this with the Expo Router typed route
constant/function (e.g., use the generated Routes enum/const for the add-funds
bridge route) so navigation is type-safe. Update the onPress in AddFunds.tsx to
call router.push with the typed route (reference the existing useRouter()
instance) and add the import for the generated route identifiers (the Routes or
route helper used across the project) so the route is not constructed as a raw
string.

In `@src/components/add-funds/Kyc.tsx`:
- Line 27: The component reads countryCode at render time which can become
stale; move the queryClient.getQueryData<string>(["user","country"]) call into
the completeOnboarding function so the mutation always reads the latest cache
value. Update the code to remove the top-level countryCode constant and instead
call queryClient.getQueryData inside completeOnboarding (referencing the same
query key), then use that local countryCode variable in the existing mutation
payload/logic.

In `@src/components/add-funds/Onboard.tsx`:
- Around line 70-76: The code assumes providers.providers.manteca always exists
and then reads its status into newStatus, which can throw if manteca is missing;
update the logic around the fetchQuery result (the providers variable returned
by queryClient.fetchQuery / getRampProviders) to null-check or use optional
chaining for providers.providers.manteca before accessing .status and provide a
safe fallback (e.g., undefined or a default status) and handle that fallback
where newStatus is used so missing manteca entries won't cause a runtime error.

In `@src/components/add-funds/Status.tsx`:
- Around line 30-34: The code uses raw path strings in Redirect and
router.replace (see Redirect and handleClose/router.replace) which breaks the
typed-route convention; replace the string routes with the app's typed route
helpers (use the generated typed routes or route constants) so Redirect gets a
typed route prop and handleClose calls router.replace with the corresponding
typed route value instead of "/add-funds" or "/(main)/(home)"; update the
imports to pull the typed routes (or route helper functions) and use those
symbols wherever validCurrency gating currently returns or navigates.

In `@src/utils/server.ts`:
- Around line 302-314: The onboardingData parameter of startRampOnboarding is
overly restrictive (typed as { provider: "manteca" }); change its type to accept
the broader RampProvider (or a union that includes "manteca" and future
providers) so the function signature becomes something like
startRampOnboarding(onboardingData: { provider: RampProvider }) and update any
callers to pass a RampProvider value; keep the body (auth(), api.ramp.$post,
error handling and return response.json()) unchanged but ensure
imports/reference to RampProvider are added where startRampOnboarding is
defined.

const { t } = useTranslation();
const router = useRouter();
const { currency } = useLocalSearchParams<{ currency: string }>();
const countryCode = queryClient.getQueryData<string>(["user", "country"]) ?? "";
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Move countryCode read into completeOnboarding to avoid stale cache reference.

Reading countryCode at component level captures the value at render time. If the cache updates during the mutation flow, completeOnboarding will use the stale value. Move the read inside completeOnboarding for the most current value.

♻️ Suggested fix
-  const countryCode = queryClient.getQueryData<string>(["user", "country"]) ?? "";
@@
   async function completeOnboarding() {
     try {
       await startRampOnboarding({ provider: "manteca" });

       await queryClient.invalidateQueries({ queryKey: ["ramp", "providers"] });

+      const countryCode = queryClient.getQueryData<string>(["user", "country"]);
       const providers = await queryClient.fetchQuery({
🤖 Prompt for AI Agents
In `@src/components/add-funds/Kyc.tsx` at line 27, The component reads countryCode
at render time which can become stale; move the
queryClient.getQueryData<string>(["user","country"]) call into the
completeOnboarding function so the mutation always reads the latest cache value.
Update the code to remove the top-level countryCode constant and instead call
queryClient.getQueryData inside completeOnboarding (referencing the same query
key), then use that local countryCode variable in the existing mutation
payload/logic.

Comment on lines +70 to +76
const providers = await queryClient.fetchQuery({
queryKey: ["ramp", "providers", countryCode],
queryFn: () => getRampProviders(countryCode),
staleTime: 0,
});

const newStatus = providers.providers.manteca.status;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add null check for manteca provider to prevent runtime error.

providers.providers.manteca is accessed without checking if the manteca key exists. If the provider response doesn't include manteca (e.g., due to country restrictions), this will throw a runtime error.

🛡️ Add defensive check
       const providers = await queryClient.fetchQuery({
         queryKey: ["ramp", "providers", countryCode],
         queryFn: () => getRampProviders(countryCode),
         staleTime: 0,
       });

-      const newStatus = providers.providers.manteca.status;
+      const mantecaProvider = providers.providers.manteca;
+      if (!mantecaProvider) {
+        router.replace({ pathname: "/add-funds/status", params: { status: "error", currency } });
+        return;
+      }
+      const newStatus = mantecaProvider.status;
🤖 Prompt for AI Agents
In `@src/components/add-funds/Onboard.tsx` around lines 70 - 76, The code assumes
providers.providers.manteca always exists and then reads its status into
newStatus, which can throw if manteca is missing; update the logic around the
fetchQuery result (the providers variable returned by queryClient.fetchQuery /
getRampProviders) to null-check or use optional chaining for
providers.providers.manteca before accessing .status and provide a safe fallback
(e.g., undefined or a default status) and handle that fallback where newStatus
is used so missing manteca entries won't cause a runtime error.

Comment on lines +302 to +314
export async function startRampOnboarding(onboardingData: { provider: "manteca" }) {
await auth();
const response = await api.ramp.$post({
json: onboardingData,
});

if (!response.ok) {
const { code } = await response.json();
throw new APIError(response.status, code);
}

return response.json();
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider making the provider parameter more flexible.

The onboardingData parameter is typed as { provider: "manteca" } which is very restrictive. If additional providers are planned, consider using the RampProvider type.

♻️ Suggested improvement
-export async function startRampOnboarding(onboardingData: { provider: "manteca" }) {
+export async function startRampOnboarding(onboardingData: { provider: RampProvider }) {
🤖 Prompt for AI Agents
In `@src/utils/server.ts` around lines 302 - 314, The onboardingData parameter of
startRampOnboarding is overly restrictive (typed as { provider: "manteca" });
change its type to accept the broader RampProvider (or a union that includes
"manteca" and future providers) so the function signature becomes something like
startRampOnboarding(onboardingData: { provider: RampProvider }) and update any
callers to pass a RampProvider value; keep the body (auth(), api.ramp.$post,
error handling and return response.json()) unchanged but ensure
imports/reference to RampProvider are added where startRampOnboarding is
defined.

Copy link

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
src/utils/server.ts (1)

178-190: 🧹 Nitpick | 🔵 Trivial

createMantecaKYC duplicates getKYCTokens logic.

createMantecaKYC is nearly identical to calling getKYCTokens("manteca", redirectURI). Consider reusing the existing function to reduce duplication.

♻️ Suggested consolidation
 export async function createMantecaKYC(redirectURI?: string) {
-  await auth();
-  const response = await api.kyc.$post({ json: { scope: "manteca", redirectURI } });
-  if (!response.ok) {
-    const { code } = (await response.json()) as { code: string };
-    throw new APIError(response.status, code);
-  }
-  return response.json();
+  return getKYCTokens("manteca", redirectURI);
 }
🤖 Fix all issues with AI agents
In `@src/components/add-funds/AddFiatButton.tsx`:
- Around line 25-46: The router.push calls in handlePress should use compact
single-line object literals; update each case (NOT_STARTED, ONBOARDING, ACTIVE)
so their router.push invocation passes a single-line object (e.g., router.push({
pathname: "/add-funds/onboard", params: { currency } })) rather than multi-line
blocks—locate handlePress and replace the multi-line objects passed to
router.push with single-line equivalents to satisfy the compact-style rule.

In `@src/components/add-funds/Onboard.tsx`:
- Line 60: CurrencyImage is being computed before the validCurrency check,
causing an undefined or invalid currency to temporarily resolve to ARS; move the
CurrencyImage resolution so it only runs after validation (i.e., compute
CurrencyImage after the validCurrency check or guard with validCurrency) by
updating the code around the CurrencyImage assignment (reference symbols:
CurrencyImage, currency, currencyImages, ARS, validCurrency) so that an
invalid/missing currency does not momentarily pick ARS before the redirect.

In `@src/components/add-funds/Ramp.tsx`:
- Line 67: The current assignment const depositInfo = data?.depositInfo[0] can
yield undefined when depositInfo is an empty array; change it to explicitly
handle empty arrays by using either data?.depositInfo?.at(0) or a length check
(e.g., data?.depositInfo?.length ? data.depositInfo[0] : undefined) so
depositInfo is deterministically undefined only when there is no element, and
update any downstream usage that assumes an element exists to handle the
explicit undefined from depositInfo in the Ramp component.
- Around line 57-63: The query currently casts currency with `currency as
Currency` even though `validCurrency` already guarantees validity; instead,
narrow the value before calling useQuery to avoid inline casts: compute a typed
variable (e.g., const typedCurrency = validCurrency ? currency as Currency :
undefined), set useQuery enabled to !!typedCurrency, and pass typedCurrency into
the queryFn for getRampQuote; reference symbols: useQuery, getRampQuote,
validCurrency, currency, and queryFn. This ensures the type is asserted once
outside the query and removes the unsafe cast inside the queryFn.

In `@src/i18n/es.json`:
- Around line 540-560: The new add-funds i18n keys were appended to the end of
es.json; move them into the existing onboarding/fiat section (or insert in
sorted order) so related strings are grouped—locate keys like "Turn {{currency}}
transfers to onchain USDC", "Transfer from accounts in your name and
automatically receive USDC in your Exa account.", "Copy and share your account
details to turn {{currency}} transfers into USDC.", "Account details",
"Beneficiary name", and "Continue verification" and insert them near the other
onboarding/fiat strings (or alphabetically among nearby keys) rather than
leaving them at the end of the file.

Comment on lines +25 to +46
function handlePress() {
switch (status) {
case "NOT_STARTED":
router.push({
pathname: "/add-funds/onboard",
params: { currency },
});
break;

case "ONBOARDING":
router.push({
pathname: "/add-funds/status",
params: { status: "ONBOARDING", currency },
});
break;

case "ACTIVE":
router.push({
pathname: "/add-funds/ramp",
params: { currency },
});
break;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Keep router.push calls compact (single-line objects).

These objects fit within print width; keeping them on one line matches the compact-style rule.
As per coding guidelines: Enforce maximally compact code style. Do not introduce line breaks inside objects, arrays, or function arguments voluntarily.

♻️ Proposed fix
-        router.push({
-          pathname: "/add-funds/onboard",
-          params: { currency },
-        });
+        router.push({ pathname: "/add-funds/onboard", params: { currency }, });
@@
-        router.push({
-          pathname: "/add-funds/status",
-          params: { status: "ONBOARDING", currency },
-        });
+        router.push({ pathname: "/add-funds/status", params: { status: "ONBOARDING", currency }, });
@@
-        router.push({
-          pathname: "/add-funds/ramp",
-          params: { currency },
-        });
+        router.push({ pathname: "/add-funds/ramp", params: { currency }, });
📝 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
function handlePress() {
switch (status) {
case "NOT_STARTED":
router.push({
pathname: "/add-funds/onboard",
params: { currency },
});
break;
case "ONBOARDING":
router.push({
pathname: "/add-funds/status",
params: { status: "ONBOARDING", currency },
});
break;
case "ACTIVE":
router.push({
pathname: "/add-funds/ramp",
params: { currency },
});
break;
function handlePress() {
switch (status) {
case "NOT_STARTED":
router.push({ pathname: "/add-funds/onboard", params: { currency }, });
break;
case "ONBOARDING":
router.push({ pathname: "/add-funds/status", params: { status: "ONBOARDING", currency }, });
break;
case "ACTIVE":
router.push({ pathname: "/add-funds/ramp", params: { currency }, });
break;
🤖 Prompt for AI Agents
In `@src/components/add-funds/AddFiatButton.tsx` around lines 25 - 46, The
router.push calls in handlePress should use compact single-line object literals;
update each case (NOT_STARTED, ONBOARDING, ACTIVE) so their router.push
invocation passes a single-line object (e.g., router.push({ pathname:
"/add-funds/onboard", params: { currency } })) rather than multi-line
blocks—locate handlePress and replace the multi-line objects passed to
router.push with single-line equivalents to satisfy the compact-style rule.

},
});

const CurrencyImage = currency ? (currencyImages[currency] ?? ARS) : ARS;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider edge case when currency is undefined.

When currency is undefined, the fallback ARS is used. However, this line executes before the validCurrency check on line 62, so an invalid/missing currency briefly resolves to ARS before redirect.

♻️ Move after validation
+  if (!validCurrency) return <Redirect href="/add-funds" />;
+
   const CurrencyImage = currency ? (currencyImages[currency] ?? ARS) : ARS;
 
-  if (!validCurrency) return <Redirect href="/add-funds" />;
🤖 Prompt for AI Agents
In `@src/components/add-funds/Onboard.tsx` at line 60, CurrencyImage is being
computed before the validCurrency check, causing an undefined or invalid
currency to temporarily resolve to ARS; move the CurrencyImage resolution so it
only runs after validation (i.e., compute CurrencyImage after the validCurrency
check or guard with validCurrency) by updating the code around the CurrencyImage
assignment (reference symbols: CurrencyImage, currency, currencyImages, ARS,
validCurrency) so that an invalid/missing currency does not momentarily pick ARS
before the redirect.

Comment on lines +57 to +63
const { data, isPending } = useQuery({
queryKey: ["ramp", "quote", "manteca", currency],
queryFn: () => getRampQuote({ provider: "manteca", currency: currency as Currency }),
enabled: validCurrency,
refetchInterval: 30_000,
staleTime: 10_000,
});
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Query enabled but currency cast bypasses type guard.

The query is enabled when validCurrency is true, but the queryFn uses currency as Currency which bypasses the type guard. Since validCurrency ensures the type is valid, consider using a type-safe pattern.

♻️ Suggested improvement
   const { data, isPending } = useQuery({
     queryKey: ["ramp", "quote", "manteca", currency],
-    queryFn: () => getRampQuote({ provider: "manteca", currency: currency as Currency }),
+    queryFn: () => {
+      if (!isValidCurrency(currency)) throw new Error("invalid currency");
+      return getRampQuote({ provider: "manteca", currency });
+    },
     enabled: validCurrency,
     refetchInterval: 30_000,
     staleTime: 10_000,
   });
🤖 Prompt for AI Agents
In `@src/components/add-funds/Ramp.tsx` around lines 57 - 63, The query currently
casts currency with `currency as Currency` even though `validCurrency` already
guarantees validity; instead, narrow the value before calling useQuery to avoid
inline casts: compute a typed variable (e.g., const typedCurrency =
validCurrency ? currency as Currency : undefined), set useQuery enabled to
!!typedCurrency, and pass typedCurrency into the queryFn for getRampQuote;
reference symbols: useQuery, getRampQuote, validCurrency, currency, and queryFn.
This ensures the type is asserted once outside the query and removes the unsafe
cast inside the queryFn.


if (!validCurrency) return <Redirect href="/add-funds" />;

const depositInfo = data?.depositInfo[0];
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Potential undefined access when depositInfo array is empty.

data?.depositInfo[0] will be undefined if the array exists but is empty. The downstream code handles undefined values, but consider adding an explicit check or using .at(0) for clarity.

♻️ Suggested improvement
-  const depositInfo = data?.depositInfo[0];
+  const depositInfo = data?.depositInfo.at(0);
📝 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
const depositInfo = data?.depositInfo[0];
const depositInfo = data?.depositInfo.at(0);
🤖 Prompt for AI Agents
In `@src/components/add-funds/Ramp.tsx` at line 67, The current assignment const
depositInfo = data?.depositInfo[0] can yield undefined when depositInfo is an
empty array; change it to explicitly handle empty arrays by using either
data?.depositInfo?.at(0) or a length check (e.g., data?.depositInfo?.length ?
data.depositInfo[0] : undefined) so depositInfo is deterministically undefined
only when there is no element, and update any downstream usage that assumes an
element exists to handle the explicit undefined from depositInfo in the Ramp
component.

Copy link

@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

🤖 Fix all issues with AI agents
In `@src/components/add-funds/AddFunds.tsx`:
- Around line 114-121: Rename the variable providerData to a more concrete noun
(e.g., provider) in the map over Object.entries(providers.providers) so the
callback signature becomes ([providerKey, provider]) and update all references
inside that callback (currencies = provider.onramp.currencies,
status={provider.status}) to use the new name; ensure the key construction
(key={`${providerKey}-${currency}`}) and the AddFiatButton usage remain
unchanged.

Comment on lines +114 to +121
{Object.entries(providers.providers).flatMap(([providerKey, providerData]) => {
const currencies = providerData.onramp.currencies;
return currencies.map((currency) => (
<AddFiatButton
key={`${providerKey}-${currency}`}
currency={currency}
status={providerData.status}
/>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Rename providerData to avoid fluff-word naming.

Line 114 uses providerData; prefer a concrete noun like provider to align with the naming guideline.

♻️ Proposed fix
-                    {Object.entries(providers.providers).flatMap(([providerKey, providerData]) => {
-                      const currencies = providerData.onramp.currencies;
+                    {Object.entries(providers.providers).flatMap(([providerKey, provider]) => {
+                      const currencies = provider.onramp.currencies;
                       return currencies.map((currency) => (
                         <AddFiatButton
                           key={`${providerKey}-${currency}`}
                           currency={currency}
-                          status={providerData.status}
+                          status={provider.status}
                         />
                       ));
                     })}

As per coding guidelines: Avoid fluff words that carry no meaningful information such as data, state, manager, engine, object, entity, instance.

📝 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
{Object.entries(providers.providers).flatMap(([providerKey, providerData]) => {
const currencies = providerData.onramp.currencies;
return currencies.map((currency) => (
<AddFiatButton
key={`${providerKey}-${currency}`}
currency={currency}
status={providerData.status}
/>
{Object.entries(providers.providers).flatMap(([providerKey, provider]) => {
const currencies = provider.onramp.currencies;
return currencies.map((currency) => (
<AddFiatButton
key={`${providerKey}-${currency}`}
currency={currency}
status={provider.status}
/>
🤖 Prompt for AI Agents
In `@src/components/add-funds/AddFunds.tsx` around lines 114 - 121, Rename the
variable providerData to a more concrete noun (e.g., provider) in the map over
Object.entries(providers.providers) so the callback signature becomes
([providerKey, provider]) and update all references inside that callback
(currencies = provider.onramp.currencies, status={provider.status}) to use the
new name; ensure the key construction (key={`${providerKey}-${currency}`}) and
the AddFiatButton usage remain unchanged.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View issue and 14 additional flags in Devin Review.

Open in Devin Review

Comment on lines +3 to +7
export const currencyMap = {
ARS: { name: "Argentinian Pesos", emoji: "🇦🇷" },
BRL: { name: "Brazilian Real", emoji: "🇧🇷" },
USD: { name: "US Dollars", emoji: "🇺🇸" },
} as const;

Choose a reason for hiding this comment

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

🔴 Currency validation mismatch causes redirect loop for non-ARS/BRL/USD currencies

The client-side currencyMap only defines ARS, BRL, and USD, but the server can return additional currencies (CLP, COP, PUSD, CRC, GTQ, MXN, PHP, BOB) based on the user's country.

Click to expand

Flow that triggers the bug:

  1. AddFunds.tsx fetches providers from server via getRampProviders()
  2. Server returns currencies based on user's country (e.g., CLP for Chile users via CurrenciesByExchange in server/utils/ramps/manteca.ts:570-581)
  3. AddFiatButton renders with currency="CLP" and user clicks it
  4. User navigates to /add-funds/onboard?currency=CLP
  5. Onboard.tsx calls isValidCurrency("CLP") which returns false because CLP is not in currencyMap
  6. User is immediately redirected back to /add-funds via <Redirect href="/add-funds" />

Actual vs Expected:

  • Actual: Users from Chile, Colombia, Panama, Costa Rica, Guatemala, Mexico, Philippines, and Bolivia cannot complete the onramp flow - they get stuck in a redirect loop.
  • Expected: All currencies returned by the server should be valid on the client side.

Impact:

Users from 8 countries cannot use the manteca ramp feature at all despite the server supporting their currencies.

Recommendation: Sync the client-side currencyMap with all currencies from the server's MantecaCurrency array, or fetch the valid currencies dynamically from the server.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@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

🤖 Fix all issues with AI agents
In `@src/components/add-funds/Ramp.tsx`:
- Around line 124-142: The onCopy callbacks passed to DetailRow (for DetailRow
props in Ramp.tsx) call copyToClipboard with variables of type string |
undefined (beneficiaryName, depositAddress, depositAlias) using
short-circuiting, which doesn't narrow types inside the callback; update each
onCopy to perform an explicit runtime type check (e.g., if (typeof
beneficiaryName === "string") copyToClipboard(beneficiaryName)) for
beneficiaryName, depositAddress and depositAlias so only a string is passed to
copyToClipboard and TypeScript type errors are resolved.

Comment on lines +124 to +142
<DetailRow
label={t("Beneficiary name")}
value={beneficiaryName}
isLoading={isPending}
onCopy={() => beneficiaryName && copyToClipboard(beneficiaryName)}
/>
<DetailRow
label={depositInfo?.displayName ?? t("Account")}
value={depositAddress}
isLoading={isPending}
onCopy={() => depositAddress && copyToClipboard(depositAddress)}
/>
{depositAlias && (
<DetailRow
label={t("Deposit alias")}
value={depositAlias}
isLoading={isPending}
onCopy={() => copyToClipboard(depositAlias)}
/>
Copy link

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:

cat -n src/components/add-funds/Ramp.tsx | sed -n '120,150p'

Repository: exactly/exa

Length of output: 1615


🏁 Script executed:

cat -n src/components/add-funds/Ramp.tsx | sed -n '1,50p'

Repository: exactly/exa

Length of output: 2022


🏁 Script executed:

rg -n "copyToClipboard" src/components/add-funds/Ramp.tsx -A 2

Repository: exactly/exa

Length of output: 670


🏁 Script executed:

rg -n "const copyToClipboard|function copyToClipboard|export.*copyToClipboard" src/ -A 3

Repository: exactly/exa

Length of output: 395


🏁 Script executed:

rg -n "beneficiaryName|depositAddress|depositAlias" src/components/add-funds/Ramp.tsx | head -20

Repository: exactly/exa

Length of output: 733


🏁 Script executed:

cat -n src/components/add-funds/Ramp.tsx | sed -n '49,120p'

Repository: exactly/exa

Length of output: 3156


Add explicit type guards inside onCopy callbacks.

The short-circuit operator (&&) and conditional rendering do not provide type narrowing inside callback scopes. Since beneficiaryName, depositAddress, and depositAlias are all string | undefined and copyToClipboard expects string, add explicit guards to narrow the types:

🛠️ Suggested fix
                 <DetailRow
                   label={t("Beneficiary name")}
                   value={beneficiaryName}
                   isLoading={isPending}
-                  onCopy={() => beneficiaryName && copyToClipboard(beneficiaryName)}
+                  onCopy={() => {
+                    if (!beneficiaryName) return;
+                    copyToClipboard(beneficiaryName);
+                  }}
                 />
                 <DetailRow
                   label={depositInfo?.displayName ?? t("Account")}
                   value={depositAddress}
                   isLoading={isPending}
-                  onCopy={() => depositAddress && copyToClipboard(depositAddress)}
+                  onCopy={() => {
+                    if (!depositAddress) return;
+                    copyToClipboard(depositAddress);
+                  }}
                 />
                 {depositAlias && (
                   <DetailRow
                     label={t("Deposit alias")}
                     value={depositAlias}
                     isLoading={isPending}
-                    onCopy={() => copyToClipboard(depositAlias)}
+                    onCopy={() => {
+                      if (!depositAlias) return;
+                      copyToClipboard(depositAlias);
+                    }}
                   />
                 )}
🤖 Prompt for AI Agents
In `@src/components/add-funds/Ramp.tsx` around lines 124 - 142, The onCopy
callbacks passed to DetailRow (for DetailRow props in Ramp.tsx) call
copyToClipboard with variables of type string | undefined (beneficiaryName,
depositAddress, depositAlias) using short-circuiting, which doesn't narrow types
inside the callback; update each onCopy to perform an explicit runtime type
check (e.g., if (typeof beneficiaryName === "string")
copyToClipboard(beneficiaryName)) for beneficiaryName, depositAddress and
depositAlias so only a string is passed to copyToClipboard and TypeScript type
errors are resolved.

@cruzdanilo cruzdanilo force-pushed the main branch 2 times, most recently from d4d2407 to 5d070cb Compare February 3, 2026 16:08
@franm91 franm91 marked this pull request as draft February 3, 2026 18:15
Copy link

@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

🤖 Fix all issues with AI agents
In `@src/components/add-funds/Onboard.tsx`:
- Around line 64-92: The completeOnboarding function is declared after it's used
in the useMutation callback which makes the flow harder to read; move the entire
async function completeOnboarding (the block that calls startRampOnboarding,
queryClient.invalidateQueries, queryClient.fetchQuery and uses router.replace
based on providers.providers.manteca.status) to be defined above the useMutation
call so the mutation references an already-declared function and the code reads
top-to-bottom.

In `@src/components/add-funds/Ramp.tsx`:
- Around line 79-82: In copyToClipboard, await the clipboard write
(setStringAsync) before showing the success toast so the user only sees
"Copied!" on actual success; on failure catch the error (reportError) and show a
failure toast or alternate feedback. Locate the copyToClipboard function and
change the flow so setStringAsync(text) is awaited (or uses .then/.catch) and
only calls toast.show(t("Copied!"), ...) inside the success branch, while error
handling calls reportError and a failure toast.

Comment on lines +64 to +92
async function completeOnboarding() {
try {
await startRampOnboarding({ provider: "manteca" });

await queryClient.invalidateQueries({ queryKey: ["ramp", "providers"] });

const providers = await queryClient.fetchQuery({
queryKey: ["ramp", "providers", countryCode],
queryFn: () => getRampProviders(countryCode),
staleTime: 0,
});

const newStatus = providers.providers.manteca.status;

if (newStatus === "ACTIVE") {
router.replace({ pathname: "/add-funds/ramp", params: { currency } });
} else if (newStatus === "ONBOARDING") {
router.replace({ pathname: "/add-funds/status", params: { status: newStatus, currency } });
} else {
router.replace({ pathname: "/add-funds/status", params: { status: "error", currency } });
}
} catch (error) {
reportError(error);
router.replace({
pathname: "/add-funds/status",
params: { status: "error", currency },
});
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

completeOnboarding is called before it's declared.

The function completeOnboarding is invoked in the mutation at line 49 but defined at line 64. While JavaScript hoisting allows this for function declarations, the current placement after the mutation that calls it can be confusing. Consider moving the function declaration before the mutation for clarity.

♻️ Suggested reordering

Move completeOnboarding declaration (lines 64-92) before the useMutation call (lines 34-58) to improve readability and make the code flow more intuitive.

🤖 Prompt for AI Agents
In `@src/components/add-funds/Onboard.tsx` around lines 64 - 92, The
completeOnboarding function is declared after it's used in the useMutation
callback which makes the flow harder to read; move the entire async function
completeOnboarding (the block that calls startRampOnboarding,
queryClient.invalidateQueries, queryClient.fetchQuery and uses router.replace
based on providers.providers.manteca.status) to be defined above the useMutation
call so the mutation references an already-declared function and the code reads
top-to-bottom.

Comment on lines +79 to +82
function copyToClipboard(text: string) {
setStringAsync(text).catch(reportError);
toast.show(t("Copied!"), { native: true, duration: 1000, burntOptions: { haptic: "success" } });
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Toast shows success before clipboard operation completes.

The toast is shown immediately after calling setStringAsync, but the async operation is caught separately. If clipboard access fails, the user still sees "Copied!" which is misleading.

🛠️ Suggested fix
   function copyToClipboard(text: string) {
-    setStringAsync(text).catch(reportError);
-    toast.show(t("Copied!"), { native: true, duration: 1000, burntOptions: { haptic: "success" } });
+    setStringAsync(text)
+      .then(() => toast.show(t("Copied!"), { native: true, duration: 1000, burntOptions: { haptic: "success" } }))
+      .catch(reportError);
   }
📝 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
function copyToClipboard(text: string) {
setStringAsync(text).catch(reportError);
toast.show(t("Copied!"), { native: true, duration: 1000, burntOptions: { haptic: "success" } });
}
function copyToClipboard(text: string) {
setStringAsync(text)
.then(() => toast.show(t("Copied!"), { native: true, duration: 1000, burntOptions: { haptic: "success" } }))
.catch(reportError);
}
🤖 Prompt for AI Agents
In `@src/components/add-funds/Ramp.tsx` around lines 79 - 82, In copyToClipboard,
await the clipboard write (setStringAsync) before showing the success toast so
the user only sees "Copied!" on actual success; on failure catch the error
(reportError) and show a failure toast or alternate feedback. Locate the
copyToClipboard function and change the flow so setStringAsync(text) is awaited
(or uses .then/.catch) and only calls toast.show(t("Copied!"), ...) inside the
success branch, while error handling calls reportError and a failure toast.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ui: ramp manteca

3 participants