-
Notifications
You must be signed in to change notification settings - Fork 13
Production to staging 130 #1680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
26fba42
3e30648
5346a59
4004acd
c088c26
578071b
23273bf
bf43ef7
ad4235d
f8d855d
226b2b7
854d669
f38365c
9f621f2
8b0697f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,13 +2,25 @@ import { AccountType } from '@/interfaces' | |||||||||||||||||||||||||||||||
| import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' | ||||||||||||||||||||||||||||||||
| import useGetExchangeRate, { type IExchangeRate } from '@/hooks/useGetExchangeRate' | ||||||||||||||||||||||||||||||||
| import { useExchangeRate } from '@/hooks/useExchangeRate' | ||||||||||||||||||||||||||||||||
| import { SYMBOLS_BY_CURRENCY_CODE } from '@/hooks/useCurrency' | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // constants for exchange rate messages, specific to ExchangeRate component | ||||||||||||||||||||||||||||||||
| const APPROXIMATE_VALUE_MESSAGE = | ||||||||||||||||||||||||||||||||
| "This is an approximate value. The actual amount received may vary based on your bank's exchange rate" | ||||||||||||||||||||||||||||||||
| const LOCAL_CURRENCY_LABEL = 'Amount you will receive' | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| interface IExchangeRateProps extends Omit<IExchangeRate, 'enabled'> { | ||||||||||||||||||||||||||||||||
| nonEuroCurrency?: string | ||||||||||||||||||||||||||||||||
| sourceCurrency?: string | ||||||||||||||||||||||||||||||||
| amountToConvert?: string | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const ExchangeRate = ({ accountType, nonEuroCurrency, sourceCurrency = 'USD' }: IExchangeRateProps) => { | ||||||||||||||||||||||||||||||||
| const ExchangeRate = ({ | ||||||||||||||||||||||||||||||||
| accountType, | ||||||||||||||||||||||||||||||||
| nonEuroCurrency, | ||||||||||||||||||||||||||||||||
| sourceCurrency = 'USD', | ||||||||||||||||||||||||||||||||
| amountToConvert, | ||||||||||||||||||||||||||||||||
| }: IExchangeRateProps) => { | ||||||||||||||||||||||||||||||||
| const { exchangeRate, isFetchingRate } = useGetExchangeRate({ accountType, enabled: !nonEuroCurrency }) | ||||||||||||||||||||||||||||||||
| const { exchangeRate: nonEruoExchangeRate, isLoading } = useExchangeRate({ | ||||||||||||||||||||||||||||||||
| sourceCurrency, | ||||||||||||||||||||||||||||||||
|
|
@@ -26,27 +38,51 @@ const ExchangeRate = ({ accountType, nonEuroCurrency, sourceCurrency = 'USD' }: | |||||||||||||||||||||||||||||||
| let displayValue = '-' | ||||||||||||||||||||||||||||||||
| let isLoadingRate = false | ||||||||||||||||||||||||||||||||
| let moreInfoText = '' | ||||||||||||||||||||||||||||||||
| let rate: number | null = null | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (nonEuroCurrency) { | ||||||||||||||||||||||||||||||||
| displayValue = nonEruoExchangeRate | ||||||||||||||||||||||||||||||||
| ? `1 ${sourceCurrency} = ${parseFloat(nonEruoExchangeRate.toString()).toFixed(4)} ${nonEuroCurrency}` | ||||||||||||||||||||||||||||||||
| : '-' | ||||||||||||||||||||||||||||||||
| isLoadingRate = isLoading | ||||||||||||||||||||||||||||||||
| moreInfoText = | ||||||||||||||||||||||||||||||||
| "This is an approximate value. The actual amount received may vary based on your bank's exchange rate" | ||||||||||||||||||||||||||||||||
| rate = nonEruoExchangeRate | ||||||||||||||||||||||||||||||||
| moreInfoText = APPROXIMATE_VALUE_MESSAGE | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| displayValue = exchangeRate ? `1 USD = ${parseFloat(exchangeRate).toFixed(4)} ${toCurrency}` : '-' | ||||||||||||||||||||||||||||||||
| isLoadingRate = isFetchingRate | ||||||||||||||||||||||||||||||||
| rate = exchangeRate ? parseFloat(exchangeRate) : null | ||||||||||||||||||||||||||||||||
| moreInfoText = `Exchange rates apply when converting to ${toCurrency}` | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // calculate local currency amount if provided | ||||||||||||||||||||||||||||||||
| let localCurrencyAmount: string | null = null | ||||||||||||||||||||||||||||||||
| if (amountToConvert && rate && rate > 0) { | ||||||||||||||||||||||||||||||||
| const amount = parseFloat(amountToConvert) | ||||||||||||||||||||||||||||||||
| if (!isNaN(amount) && amount > 0) { | ||||||||||||||||||||||||||||||||
| localCurrencyAmount = (amount * rate).toFixed(2) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+63
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against comma‑formatted inputs when parsing
🛠️ Suggested guard- const amount = parseFloat(amountToConvert)
- if (!isNaN(amount) && amount > 0) {
+ const normalized = amountToConvert.replace(/,/g, '')
+ const amount = Number(normalized)
+ if (Number.isFinite(amount) && amount > 0) {
localCurrencyAmount = (amount * rate).toFixed(2)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const currency = nonEuroCurrency || toCurrency | ||||||||||||||||||||||||||||||||
| const currencySymbol = SYMBOLS_BY_CURRENCY_CODE[currency] || currency | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||
| <PaymentInfoRow | ||||||||||||||||||||||||||||||||
| loading={isLoadingRate} | ||||||||||||||||||||||||||||||||
| label="Exchange Rate" | ||||||||||||||||||||||||||||||||
| moreInfoText={moreInfoText} | ||||||||||||||||||||||||||||||||
| value={displayValue} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||
| <PaymentInfoRow | ||||||||||||||||||||||||||||||||
| loading={isLoadingRate} | ||||||||||||||||||||||||||||||||
| label="Exchange Rate" | ||||||||||||||||||||||||||||||||
| moreInfoText={moreInfoText} | ||||||||||||||||||||||||||||||||
| value={displayValue} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| {localCurrencyAmount && ( | ||||||||||||||||||||||||||||||||
| <PaymentInfoRow | ||||||||||||||||||||||||||||||||
| loading={isLoadingRate} | ||||||||||||||||||||||||||||||||
| label={LOCAL_CURRENCY_LABEL} | ||||||||||||||||||||||||||||||||
| value={`~ ${currencySymbol}${localCurrencyAmount}`} | ||||||||||||||||||||||||||||||||
| moreInfoText={APPROXIMATE_VALUE_MESSAGE} | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,7 +3,7 @@ import { useAppDispatch, useUserStore } from '@/redux/hooks' | |||||||||||||||||||||||||||||||||||||||||||||
| import { userActions } from '@/redux/slices/user-slice' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { fetchWithSentry } from '@/utils/sentry.utils' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { hitUserMetric } from '@/utils/metrics.utils' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { keepPreviousData, useQuery } from '@tanstack/react-query' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { useQuery } from '@tanstack/react-query' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { usePWAStatus } from '../usePWAStatus' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { useDeviceType } from '../useGetDeviceType' | ||||||||||||||||||||||||||||||||||||||||||||||
| import { USER } from '@/constants/query.consts' | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -29,14 +29,9 @@ export const useUserQuery = (dependsOn: boolean = true) => { | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return userData | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| // RECOVERY FIX: Log error status for debugging | ||||||||||||||||||||||||||||||||||||||||||||||
| if (userResponse.status === 400 || userResponse.status === 500) { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to fetch user with error status:', userResponse.status) | ||||||||||||||||||||||||||||||||||||||||||||||
| // This indicates a backend issue - user might be in broken state | ||||||||||||||||||||||||||||||||||||||||||||||
| // The KernelClientProvider recovery logic will handle cleanup | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.warn('Failed to fetch user. Probably not logged in.') | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| console.warn('Failed to fetch user, status:', userResponse.status) | ||||||||||||||||||||||||||||||||||||||||||||||
| // clear stale redux data so the app doesn't keep serving cached user | ||||||||||||||||||||||||||||||||||||||||||||||
| dispatch(userActions.setUser(null)) | ||||||||||||||||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
31
to
36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clearing user data on any fetch failure may cause unintended logouts. The current implementation clears Redux user data for all non-OK responses, including temporary network issues (500, timeouts, connection errors). This could log users out during transient server issues or brief connectivity problems. Consider differentiating between authentication failures (401/403) and server/network errors: 🛡️ Proposed fix to preserve user data on transient errors } else {
- console.warn('Failed to fetch user, status:', userResponse.status)
- // clear stale redux data so the app doesn't keep serving cached user
- dispatch(userActions.setUser(null))
+ console.warn('Failed to fetch user, status:', userResponse.status)
+ // only clear user data on auth failures, not transient server errors
+ if (userResponse.status === 401 || userResponse.status === 403) {
+ dispatch(userActions.setUser(null))
+ }
return null
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -45,25 +40,13 @@ export const useUserQuery = (dependsOn: boolean = true) => { | |||||||||||||||||||||||||||||||||||||||||||||
| queryKey: [USER], | ||||||||||||||||||||||||||||||||||||||||||||||
| queryFn: fetchUser, | ||||||||||||||||||||||||||||||||||||||||||||||
| retry: 0, | ||||||||||||||||||||||||||||||||||||||||||||||
| // Enable if dependsOn is true (defaults to true) and no Redux user exists yet | ||||||||||||||||||||||||||||||||||||||||||||||
| enabled: dependsOn && !authUser?.user.userId, | ||||||||||||||||||||||||||||||||||||||||||||||
| // Two-tier caching strategy for optimal performance: | ||||||||||||||||||||||||||||||||||||||||||||||
| // TIER 1: TanStack Query in-memory cache (5 min) | ||||||||||||||||||||||||||||||||||||||||||||||
| // - Zero latency for active sessions | ||||||||||||||||||||||||||||||||||||||||||||||
| // - Lost on page refresh (intentional - forces SW cache check) | ||||||||||||||||||||||||||||||||||||||||||||||
| // TIER 2: Service Worker disk cache (1 week StaleWhileRevalidate) | ||||||||||||||||||||||||||||||||||||||||||||||
| // - <50ms response on cold start/offline | ||||||||||||||||||||||||||||||||||||||||||||||
| // - Persists across sessions | ||||||||||||||||||||||||||||||||||||||||||||||
| // Flow: TQ cache → if stale → fetch() → SW intercepts → SW cache → Network | ||||||||||||||||||||||||||||||||||||||||||||||
| staleTime: 5 * 60 * 1000, // 5 min (balance: fresh enough + reduces SW hits) | ||||||||||||||||||||||||||||||||||||||||||||||
| gcTime: 10 * 60 * 1000, // Keep unused data 10 min before garbage collection | ||||||||||||||||||||||||||||||||||||||||||||||
| // Refetch on mount - TQ automatically skips if data is fresh (< staleTime) | ||||||||||||||||||||||||||||||||||||||||||||||
| enabled: dependsOn, | ||||||||||||||||||||||||||||||||||||||||||||||
| staleTime: 5 * 60 * 1000, | ||||||||||||||||||||||||||||||||||||||||||||||
| gcTime: 10 * 60 * 1000, | ||||||||||||||||||||||||||||||||||||||||||||||
| refetchOnMount: true, | ||||||||||||||||||||||||||||||||||||||||||||||
| // Refetch on focus - TQ automatically skips if data is fresh (< staleTime) | ||||||||||||||||||||||||||||||||||||||||||||||
| refetchOnWindowFocus: true, | ||||||||||||||||||||||||||||||||||||||||||||||
| // Initialize with Redux data if available (hydration) | ||||||||||||||||||||||||||||||||||||||||||||||
| initialData: authUser || undefined, | ||||||||||||||||||||||||||||||||||||||||||||||
| // Keep previous data during refetch (smooth UX, no flicker) | ||||||||||||||||||||||||||||||||||||||||||||||
| placeholderData: keepPreviousData, | ||||||||||||||||||||||||||||||||||||||||||||||
| // use redux data as placeholder while fetching (no flicker) | ||||||||||||||||||||||||||||||||||||||||||||||
| // but always validate against the backend | ||||||||||||||||||||||||||||||||||||||||||||||
| placeholderData: authUser || undefined, | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "jwt-token" --type ts --type tsx --type js --type jsx -B 2 -A 2Repository: peanutprotocol/peanut-ui
Length of output: 95
🏁 Script executed:
rg -n "Set-Cookie" --type ts --type tsx --type js --type jsx -B 2 -A 2Repository: peanutprotocol/peanut-ui
Length of output: 95
🏁 Script executed:
Repository: peanutprotocol/peanut-ui
Length of output: 849
🏁 Script executed:
rg "jwt-token" -n -B 2 -A 2Repository: peanutprotocol/peanut-ui
Length of output: 20501
🏁 Script executed:
Repository: peanutprotocol/peanut-ui
Length of output: 2448
🏁 Script executed:
Repository: peanutprotocol/peanut-ui
Length of output: 1092
🏁 Script executed:
cat -n src/app/api/peanut/user/get-user-from-cookie/route.ts | head -40Repository: peanutprotocol/peanut-ui
Length of output: 1761
Add Secure flag to cookie-clear Set-Cookie header in production.
The original
jwt-tokencookie is set withsecure: process.env.NODE_ENV === 'production', but the clear operation at line 31 omits the Secure flag. IncludeSecurein the Set-Cookie header to maintain consistency:This ensures proper deletion in production environments and aligns with the original cookie configuration.
🤖 Prompt for AI Agents