Dev - #2
Conversation
📝 WalkthroughWalkthroughThe PR adds Supabase-backed property search with shared filters, result states, and authenticated client access. It adds property detail and map screens, saved-property and administrative actions, navigation from property cards, a create-property tab placeholder, and list rendering optimizations. ChangesProperty experience
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant SearchScreen
participant SearchList
participant PropertyCard
participant PropertyPage
participant Supabase
participant MapScreen
SearchScreen->>Supabase: fetch filtered properties
Supabase-->>SearchScreen: return property results
SearchScreen->>SearchList: render results
SearchList->>PropertyCard: render property item
PropertyCard->>PropertyPage: navigate with property ID
PropertyPage->>Supabase: fetch property details
Supabase-->>PropertyPage: return property data
PropertyPage->>MapScreen: navigate with location parameters
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (5)
components/(root)/(tabs)/search/filter-modal/price-ranges.tsx (1)
73-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the duplicate utility classes.
chip(isActive)already returnspx-4 py-2 rounded-full border. The wrapper template addspx-1 py-1.5 rounded-full borderbefore it, so padding classes conflict and the resolved padding is not obvious. Apply the helper output alone, or add a size variant to the helper inconstants/modal-filter.ts.♻️ Proposed refactor
- className={`px-1 py-1.5 rounded-full border ${chip(isActive)}`} + className={chip(isActive)} style={shadow} > - <Text className={`font-bold ${chipText(isActive)}`}> + <Text className={chipText(isActive)}>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/`(root)/(tabs)/search/filter-modal/price-ranges.tsx around lines 73 - 76, Remove the duplicate utility classes from the wrapper using chip(isActive) in the price-range filter UI. Apply the chip(isActive) output alone, or update chip in constants/modal-filter.ts to own the size variant, ensuring padding, rounding, and border classes are defined only once.components/(root)/(tabs)/search/filter-modal/index.tsx (1)
28-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the unused
PRICE_INPUTSarray.This array is never referenced in the JSX of this component. The same array exists in
components/(root)/(tabs)/search/filter-modal/price-ranges.tsxlines 25-28, which is where it is rendered. Delete the copy here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/`(root)/(tabs)/search/filter-modal/index.tsx around lines 28 - 31, Remove the unused PRICE_INPUTS array from the filter modal component, including its localMin, localMax, setLocalMin, and setLocalMax references; retain the rendered definition in price-ranges.tsx unchanged.components/(root)/(tabs)/search/filter-chip.tsx (1)
4-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueType
iconagainst the Ionicons glyph map.
anyallows an invalid icon name to pass type checking. An invalid name renders nothing at runtime.♻️ Proposed refactor
type FilterChipProps = { label: string, - icon?: any, + icon?: keyof typeof Ionicons.glyphMap, onPress: () => void, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/`(root)/(tabs)/search/filter-chip.tsx around lines 4 - 8, Update the icon property in FilterChipProps to use the Ionicons glyph-map key type instead of any, ensuring only valid Ionicons names are accepted while preserving its optional nature.components/(root)/(tabs)/search/filter-modal/chip.tsx (1)
4-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider generics instead of
any.
data: any[]andselected: anyallow a mismatch between the option values and the setter. A generic parameter keeps the call sites in filter-modal/index.tsx type-checked.♻️ Proposed refactor
-const Chips = ( - { label, data, selected, setSelected }: { - label: string, - data: any[], - selected: any, - setSelected: (value: any) => void - }) => { +const Chips = <T,>( + { label, data, selected, setSelected }: { + label: string, + data: { label: string; value: T }[], + selected: T, + setSelected: (value: T) => void + }) => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/`(root)/(tabs)/search/filter-modal/chip.tsx around lines 4 - 10, Update the Chips component’s props to use a generic option type instead of any for data, selected, and setSelected, ensuring the setter accepts the same type as the selected value and array elements. Preserve existing call-site behavior while allowing filter-modal/index.tsx to type-check option values consistently.hooks/useSupabase.ts (1)
8-10: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winStabilize the Supabase client across renders.
@clerk/expo@4.2.1creates a newgetTokenwrapper on eachuseAuth()call. The current dependency can therefore recreate theSupabaseClienton every render and discard client-local resources. Store the latest getter in a ref and create the client once.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hooks/useSupabase.ts` around lines 8 - 10, Update the client initialization in useSupabase to store the latest getToken function in a ref, while creating the SupabaseClient only once with a stable memoization dependency. Ensure the client’s token callback reads the current ref value so token retrieval remains up to date without recreating the client across renders.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/`(root)/(tabs)/create.tsx:
- Around line 5-6: Update the placeholder text rendered by the CreateProperty
component to use the user-facing “Add Property” label, matching the tab title
defined in the tabs layout, instead of the internal component identifier.
In `@app/`(root)/(tabs)/search.tsx:
- Around line 55-85: Update fetchProperties and its useEffect to reset loading
in a finally block, including when the request fails, and guard
setProperties/setLoading with a cancellation flag so stale responses cannot
update state after dependencies change or the effect unmounts. Debounce the
search term in the effect before invoking fetchProperties, while preserving the
existing filters and ordering.
- Around line 60-62: Update the search query construction in the search filter
block to escape backslashes and double quotes in search, then quote each ilike
value before interpolating it into query.or. Preserve searching both title and
city while preventing reserved characters from altering the PostgREST filter
expression.
In `@components/`(root)/(tabs)/search/filter-modal/chip.tsx:
- Around line 18-22: Update the key in the data mapping within the chip
component to use each option’s unique label instead of item.value, ensuring the
initial null-valued entries receive non-null React keys while preserving the
existing selection and press behavior.
In `@components/`(root)/(tabs)/search/filter-modal/index.tsx:
- Around line 47-55: Update handleApply in
components/(root)/(tabs)/search/filter-modal/index.tsx:47-55 to parse both local
price fields, write valid numbers or null through setMinPrice and setMaxPrice,
and reject non-numeric input. In
components/(root)/(tabs)/search/filter-modal/price-ranges.tsx:61-81, remove
preset writes to the store, update only setLocalMin and setLocalMax, and derive
isActive from localMin/localMax so Apply is the sole store commit path.
- Around line 25-26: Synchronize localMin and localMax with minPrice and
maxPrice whenever the filter modal opens, rather than relying only on the
useState initializers. Update the modal’s open/visible handling in the component
so reopening after resetFilters or chip clearing reflects the current store
values, while preserving the existing string formatting and empty-value
behavior.
In `@components/`(root)/(tabs)/search/list/index.tsx:
- Around line 29-32: Update the ActivityIndicator in the loading state to use
its color prop with the intended blue value, and remove the ineffective
text-blue-500 className so the indicator renders with the specified tint.
---
Nitpick comments:
In `@components/`(root)/(tabs)/search/filter-chip.tsx:
- Around line 4-8: Update the icon property in FilterChipProps to use the
Ionicons glyph-map key type instead of any, ensuring only valid Ionicons names
are accepted while preserving its optional nature.
In `@components/`(root)/(tabs)/search/filter-modal/chip.tsx:
- Around line 4-10: Update the Chips component’s props to use a generic option
type instead of any for data, selected, and setSelected, ensuring the setter
accepts the same type as the selected value and array elements. Preserve
existing call-site behavior while allowing filter-modal/index.tsx to type-check
option values consistently.
In `@components/`(root)/(tabs)/search/filter-modal/index.tsx:
- Around line 28-31: Remove the unused PRICE_INPUTS array from the filter modal
component, including its localMin, localMax, setLocalMin, and setLocalMax
references; retain the rendered definition in price-ranges.tsx unchanged.
In `@components/`(root)/(tabs)/search/filter-modal/price-ranges.tsx:
- Around line 73-76: Remove the duplicate utility classes from the wrapper using
chip(isActive) in the price-range filter UI. Apply the chip(isActive) output
alone, or update chip in constants/modal-filter.ts to own the size variant,
ensuring padding, rounding, and border classes are defined only once.
In `@hooks/useSupabase.ts`:
- Around line 8-10: Update the client initialization in useSupabase to store the
latest getToken function in a ref, while creating the SupabaseClient only once
with a stable memoization dependency. Ensure the client’s token callback reads
the current ref value so token retrieval remains up to date without recreating
the client across renders.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ace78cdd-1790-4cd8-a594-c10e5405dc29
📒 Files selected for processing (14)
app/(root)/(tabs)/_layout.tsxapp/(root)/(tabs)/create.tsxapp/(root)/(tabs)/search.tsxcomponents/(root)/(tabs)/search/filter-chip.tsxcomponents/(root)/(tabs)/search/filter-modal/chip.tsxcomponents/(root)/(tabs)/search/filter-modal/header.tsxcomponents/(root)/(tabs)/search/filter-modal/index.tsxcomponents/(root)/(tabs)/search/filter-modal/price-ranges.tsxcomponents/(root)/(tabs)/search/list/index.tsxcomponents/(root)/(tabs)/search/search-bar.tsxconstants/modal-filter.tshooks/useSupabase.tshooks/useUserSync.tsstore/filterStore.ts
| <View> | ||
| <Text>CreateProperty</Text> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use user-facing text for the placeholder.
CreateProperty is an internal component identifier. The tab title in app/(root)/(tabs)/_layout.tsx is Add Property. Use the same label or provide a user-facing placeholder message.
Proposed fix
- <Text>CreateProperty</Text>
+ <Text>Add Property</Text>📝 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.
| <View> | |
| <Text>CreateProperty</Text> | |
| <View> | |
| <Text>Add Property</Text> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(root)/(tabs)/create.tsx around lines 5 - 6, Update the placeholder text
rendered by the CreateProperty component to use the user-facing “Add Property”
label, matching the tab title defined in the tabs layout, instead of the
internal component identifier.
| const fetchProperties = async () => { | ||
| setLoading(true) | ||
|
|
||
| let query = supabase.from('properties').select(`*`); | ||
|
|
||
| if (search) { | ||
| query = query.or(`title.ilike.%${search}%,city.ilike.%${search}%`) | ||
| } | ||
|
|
||
| if (type) query = query.eq('type', type); | ||
| if (bedrooms) query = query.eq('bedrooms', bedrooms); | ||
| if (minPrice) query = query.gte('price', minPrice); | ||
| if (maxPrice) query = query.lte('price', maxPrice); | ||
|
|
||
| const { data, error } = await query.order('created_at', { ascending: false }); | ||
|
|
||
| if (error) { | ||
| console.error('Error fetching properties:', error); | ||
| return; | ||
| } | ||
|
|
||
| if (data) { | ||
| setProperties(data); | ||
| } | ||
|
|
||
| setLoading(false); | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| fetchProperties(); | ||
| }, [search, type, bedrooms, minPrice, maxPrice]) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Reset loading in a finally block and discard stale responses.
Two defects exist in this fetch path:
- The early
returnon line 73 skipssetLoading(false). After one failed request, the list header stays on "Searching..." permanently. - The effect reruns on every keystroke in the search field. Responses can resolve out of order, so an older result set can overwrite a newer one. There is no debounce either, so each character issues a request.
Move the reset into finally, gate the state writes behind a cancellation flag, and debounce the search term.
🐛 Proposed fix
- const fetchProperties = async () => {
+ const fetchProperties = async (isActive: () => boolean) => {
setLoading(true)
let query = supabase.from('properties').select(`*`);
@@
- const { data, error } = await query.order('created_at', { ascending: false });
-
- if (error) {
- console.error('Error fetching properties:', error);
- return;
- }
-
- if (data) {
- setProperties(data);
- }
-
- setLoading(false);
+ try {
+ const { data, error } = await query.order('created_at', { ascending: false });
+ if (!isActive()) return;
+ if (error) {
+ console.error('Error fetching properties:', error);
+ return;
+ }
+ if (data) {
+ setProperties(data);
+ }
+ } finally {
+ if (isActive()) setLoading(false);
+ }
}
useEffect(() => {
- fetchProperties();
+ let active = true;
+ const timer = setTimeout(() => fetchProperties(() => active), 300);
+ return () => {
+ active = false;
+ clearTimeout(timer);
+ };
}, [search, type, bedrooms, minPrice, maxPrice])📝 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.
| const fetchProperties = async () => { | |
| setLoading(true) | |
| let query = supabase.from('properties').select(`*`); | |
| if (search) { | |
| query = query.or(`title.ilike.%${search}%,city.ilike.%${search}%`) | |
| } | |
| if (type) query = query.eq('type', type); | |
| if (bedrooms) query = query.eq('bedrooms', bedrooms); | |
| if (minPrice) query = query.gte('price', minPrice); | |
| if (maxPrice) query = query.lte('price', maxPrice); | |
| const { data, error } = await query.order('created_at', { ascending: false }); | |
| if (error) { | |
| console.error('Error fetching properties:', error); | |
| return; | |
| } | |
| if (data) { | |
| setProperties(data); | |
| } | |
| setLoading(false); | |
| } | |
| useEffect(() => { | |
| fetchProperties(); | |
| }, [search, type, bedrooms, minPrice, maxPrice]) | |
| const fetchProperties = async (isActive: () => boolean) => { | |
| setLoading(true) | |
| let query = supabase.from('properties').select(`*`); | |
| if (search) { | |
| query = query.or(`title.ilike.%${search}%,city.ilike.%${search}%`) | |
| } | |
| if (type) query = query.eq('type', type); | |
| if (bedrooms) query = query.eq('bedrooms', bedrooms); | |
| if (minPrice) query = query.gte('price', minPrice); | |
| if (maxPrice) query = query.lte('price', maxPrice); | |
| try { | |
| const { data, error } = await query.order('created_at', { ascending: false }); | |
| if (!isActive()) return; | |
| if (error) { | |
| console.error('Error fetching properties:', error); | |
| return; | |
| } | |
| if (data) { | |
| setProperties(data); | |
| } | |
| } finally { | |
| if (isActive()) setLoading(false); | |
| } | |
| } | |
| useEffect(() => { | |
| let active = true; | |
| const timer = setTimeout(() => fetchProperties(() => active), 300); | |
| return () => { | |
| active = false; | |
| clearTimeout(timer); | |
| }; | |
| }, [search, type, bedrooms, minPrice, maxPrice]) |
🧰 Tools
🪛 React Doctor (0.9.3)
[error] 80-80: This resets a loading/busy flag only on the success path: if the awaited call rejects the reset never runs and the flag stays stuck truthy (a spinner that never stops, a button disabled forever). Move the reset into a finally block, or mirror it on every catch, so it clears on rejection too.
A trailing setLoading(false) after an await never runs if the awaited call rejects, so the flag stays stuck truthy; reset it in a finally block (or mirror the reset on every catch) so it clears on both paths.
(no-loading-flag-reset-outside-finally)
[error] 83-83: This setter runs after await, so overlapping re-runs of the effect can resolve out of order and write stale state; gate it behind a cancellation/ignore flag or return a cleanup that cancels the work.
In a useEffect whose dependencies can change, guard any setter call that runs after an await behind a cancellation/ignore flag, or return a cleanup that cancels the async work.
(no-set-state-after-await-in-effect)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(root)/(tabs)/search.tsx around lines 55 - 85, Update fetchProperties
and its useEffect to reset loading in a finally block, including when the
request fails, and guard setProperties/setLoading with a cancellation flag so
stale responses cannot update state after dependencies change or the effect
unmounts. Debounce the search term in the effect before invoking
fetchProperties, while preserving the existing filters and ordering.
Source: Linters/SAST tools
| if (search) { | ||
| query = query.or(`title.ilike.%${search}%,city.ilike.%${search}%`) | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
PostgREST or() filter escaping reserved characters supabase-js double quotes
💡 Result:
In PostgREST filters (used by Supabase), you must escape reserved characters and also handle embedded quotes/backslashes correctly. 1) When the filter value contains PostgREST reserved characters, wrap the whole value in percent-encoded double quotes (%22...%22) - PostgREST’s URL grammar says that if filters include reserved characters (,, ., :, *, (, )), you have to surround them with percent-encoded double quotes %22 for correct processing [1][2]. - Example from PostgREST docs: in.("Hebdon,John","Williams,Mary") is represented as %22Hebdon,John%22,%22Williams,Mary%22 inside the in expression [1][2]. 2) Inside those quoted filter values, escape embedded double quotes and backslashes with backslash escaping - If the value filtered by the in operator has a double quote ("), you can escape it using a backslash (\") [1]. - A backslash itself can be used with a double backslash (\\) [1]. - Supabase’s supabase-js fixed client-side escaping to match PostgREST: inside the wrapping double quotes, it escapes " and \ as \" and \\ (backslash first), not CSV-style "" doubling [3][4]. 3) Supabase-js (postgrest-js) behavior: reserved characters trigger double-quoting; embedded " and \ are escaped when using filter builders that quote - In the current supabase-js PostgrestFilterBuilder, values containing reserved characters are wrapped in double quotes (e.g., "..." in the JS string it builds) for in/notIn [5]. - Recent supabase-js changes also added proper escaping of embedded " and \ inside those quoted filter values for in()/notIn() (and related operators), following PostgREST’s quoted-element parser convention [3][4]. 4) Supabase-js .filter() with operator strings: quote/sanitize the value yourself if you use raw syntax - Supabase docs note that when you use .filter() with a generic operator/value, the operator and value are used “as-is” and need to follow PostgREST syntax and be properly sanitized [6]. Practical example (raw PostgREST URL syntax) - Suppose you want in with values that include commas and quotes: a"b,c. - PostgREST expects the values wrapped in %22...%22 when reserved chars are present, and embedded " should be escaped as \" and \ as \\ inside the quoted value [1]. If you tell me the exact supabase-js method you’re using (in(), notIn(), like(), or .filter('col','in', ...) / .or(...)) and the value you’re trying to filter (especially if it contains , ( ) . : " \), I can show the exact string you should pass so it serializes correctly.
Citations:
- 1: https://docs.postgrest.org/en/stable/references/api/url_grammar.html
- 2: https://docs.postgrest.org/en/latest/references/api/url_grammar.html
- 3: fix(postgrest): escape " and \ inside quoted filter values supabase/supabase-js#2529
- 4: fix(postgrest): escape embedded quotes and backslashes in in()/notIn() filter values supabase/supabase-js#2489
- 5: https://github.com/supabase/supabase-js/blob/21e9d4b7/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts
- 6: https://supabase.com/docs/reference/javascript/using-filters-filter
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
sed -n '1,120p' 'app/(root)/(tabs)/search.tsx'
printf '%s\n' '--- package metadata ---'
if [ -f package.json ]; then
cat package.json
fi
printf '%s\n' '--- filter API usage ---'
rg -n --glob '*.{js,jsx,ts,tsx}' '\.(or|ilike|textSearch|filter)\(' .
printf '%s\n' '--- installed or vendored PostgREST client sources ---'
rg -n --hidden --glob '!node_modules/**' --glob '!dist/**' 'class PostgrestFilterBuilder|function reservedCharacters|reservedCharacters|escape.*value|ilike' . | head -200Repository: Ijisrael42/kribb
Length of output: 6594
🌐 Web query:
site:github.com/supabase/supabase-js v2.112.1 PostgrestFilterBuilder or(filters) ilike quote escape
💡 Result:
In PostgrestFilterBuilder, filter values—including those used in ilike and related operations—must adhere to PostgREST's URL grammar for reserved characters [1][2]. When a filter value contains reserved characters (such as commas, colons, or parentheses), it must be enclosed in double quotes [1][2]. To handle internal double quotes (") or backslashes () within these quoted values, the library uses backslash escaping rather than CSV-style doubling [1][2]. Specifically: - A double quote (") is escaped as " [1][2]. - A backslash () is escaped as \ [1][2]. Recent updates to the library have ensured that PostgrestFilterBuilder automatically manages this quoting and escaping [1][2]. Methods such as ilikeAllOf and ilikeAnyOf now wrap patterns in double quotes if they contain reserved characters, and they apply the necessary backslash escaping to any internal double quotes or backslashes [1][2]. This prevents issues where reserved characters might otherwise cause the filter value to be incorrectly split or lead to parsing errors in the PostgREST API [1][2].
Citations:
- 1: fix(postgrest): escape " and \ inside quoted filter values supabase/supabase-js#2529
- 2: supabase/supabase-js@1f08eea
🌐 Web query:
site:docs.postgrest.org/en/stable/references/api/url_grammar.html ilike quoted value wildcard double quotes or
💡 Result:
In PostgreSQL, the ILIKE operator performs pattern matching using the percent sign (%) as a wildcard for any sequence of characters and the underscore (_) as a wildcard for any single character. Double quotes are not treated as special wildcard characters in PostgreSQL; they are typically used to quote identifiers like table or column names. If you need to match a literal percent sign or underscore within an ILIKE pattern, you must escape them. PostgreSQL allows you to define an escape character using the ESCAPE clause [1]. For example, to match a literal string containing a percent sign: SELECT * FROM table_name WHERE column_name ILIKE '%100!%%' ESCAPE '!'; In this example, the exclamation mark (!) is defined as the escape character, allowing the ILIKE operator to treat the following percent sign as a literal character rather than a wildcard. If you are working with tools that wrap SQL queries, such as an API layer, you may need to ensure special characters are properly encoded or escaped according to that specific tool's requirements [1].
Citations:
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import json, urllib.request
urls = [
"https://raw.githubusercontent.com/supabase/postgrest-js/v1.19.2/src/PostgrestFilterBuilder.ts",
"https://raw.githubusercontent.com/supabase/supabase-js/v2.112.1/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts",
"https://raw.githubusercontent.com/supabase/supabase-js/v2.112.1/packages/core/postgrest-js/src/PostgrestQueryBuilder.ts",
]
for url in urls:
print(f"--- {url} ---")
try:
data = urllib.request.urlopen(url, timeout=10).read().decode()
for i, line in enumerate(data.splitlines(), 1):
if "or(" in line or "filters" in line or "URLSearchParams" in line:
start=max(1,i-5); end=min(len(data.splitlines()),i+12)
lines=data.splitlines()
print("\n".join(f"{n}: {lines[n-1]}" for n in range(start,end+1)))
print()
except Exception as e:
print(type(e).__name__, e)
PYRepository: Ijisrael42/kribb
Length of output: 945
🌐 Web query:
PostgREST URL grammar quoted string like ilike wildcard percent syntax
💡 Result:
In PostgREST, you can use the like or ilike filter operators for pattern matching [1][2]. To avoid the need for percent-encoding the standard PostgreSQL percent sign (%) wildcard character, PostgREST allows you to use an asterisk (*) as an alias for the percent sign within your patterns [1][3]. For example, to filter for strings that start with "MS", you can use: /vulnerabilities?column=like.MS If your filter value includes reserved PostgREST characters (such as commas, dots, colons, parentheses, or the asterisk itself), you must surround the value in double quotes [4][5]. To ensure these quotes are processed correctly in a URL, they should be percent-encoded as %22 [4][5]. For instance, to use the like operator with a value containing reserved characters, you would format it as follows: /table?column=like.%22your*pattern%22 Some HTTP client libraries (like axios) may handle URL encoding automatically; in such environments, you may be able to use standard double quotes ("") instead of the encoded version [4][6].
Citations:
- 1: https://docs.postgrest.org/en/latest/references/api/tables_views.html
- 2: https://docs.postgrest.org/en/v12/references/api/tables_views.html
- 3: https://docs.postgrest.org/en/v13/references/api/tables_views.html
- 4: https://docs.postgrest.org/en/v12/references/api/url_grammar.html
- 5: https://docs.postgrest.org/en/stable/references/api/url_grammar.html
- 6: https://docs.postgrest.org/en/latest/references/api/url%5Fgrammar.html
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import urllib.request
urls = [
"https://raw.githubusercontent.com/supabase/supabase-js/v2.112.1/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts",
"https://raw.githubusercontent.com/supabase/supabase-js/v2.112.1/packages/core/postgrest-js/src/PostgrestQueryBuilder.ts",
]
for url in urls:
print(f"--- {url} ---")
try:
text = urllib.request.urlopen(url, timeout=15).read().decode()
lines = text.splitlines()
for i, line in enumerate(lines, 1):
if "or(" in line or "URLSearchParams" in line or "filters" in line:
lo, hi = max(1, i-8), min(len(lines), i+18)
print("\n".join(f"{n}: {lines[n-1]}" for n in range(lo, hi+1)))
print()
except Exception as exc:
print(f"{type(exc).__name__}: {exc}")
PYRepository: Ijisrael42/kribb
Length of output: 701
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- dependency lockfiles ---'
git ls-files | rg '(^|/)(package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml)$' || true
printf '%s\n' '--- exact supabase-js source ---'
for url in \
'https://raw.githubusercontent.com/supabase/supabase-js/v2.112.1/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts' \
'https://raw.githubusercontent.com/supabase/supabase-js/v2.112.1/packages/core/postgrest-js/src/PostgrestQueryBuilder.ts'
do
echo "--- $url ---"
curl -k -fsSL "$url" | nl -ba | awk '
/or\(/ || /URLSearchParams/ || /searchParams/ || /filters/ {
start=$1-6; if (start<1) start=1
end=$1+14
for (i=start; i<=end; i++) wanted[i]=1
}
{ lines[$1]=$0; max=$1 }
END {
last=0
for (i=1; i<=max; i++)
if (wanted[i] && i>last) {
for (j=i; j<=max && j<=i+20; j++) print lines[j]
print ""
last=i+20
}
}'
done
printf '%s\n' '--- PostgREST grammar excerpts ---'
curl -k -fsSL 'https://raw.githubusercontent.com/PostgREST/postgrest/v12.2/docs/references/api/url_grammar.rst' |
rg -n -C 5 'reserved|quoted|ilike|asterisk|backslash|double quote' | head -180Repository: Ijisrael42/kribb
Length of output: 900
Escape the search term before constructing the .or() filter.
search is inserted directly into PostgREST filter syntax. Reserved characters such as ,, ., (, and ) can alter the filter expression or cause a 400 response. Quote both ilike values and escape embedded " and \ characters before interpolation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(root)/(tabs)/search.tsx around lines 60 - 62, Update the search query
construction in the search filter block to escape backslashes and double quotes
in search, then quote each ilike value before interpolating it into query.or.
Preserve searching both title and city while preventing reserved characters from
altering the PostgREST filter expression.
| {data.map((item) => ( | ||
| <TouchableOpacity | ||
| key={item.value} | ||
| onPress={() => setSelected(item.value)} | ||
| className={chip(selected === item.value)} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use a non-null list key.
TYPES and BEDS both start with an entry whose value is null (constants/modal-filter.ts lines 4 and 12). React treats a null key as a missing key and logs a warning. Use the label, which is unique in both option lists.
🐛 Proposed fix
{data.map((item) => (
<TouchableOpacity
- key={item.value}
+ key={item.label}
onPress={() => setSelected(item.value)}📝 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.
| {data.map((item) => ( | |
| <TouchableOpacity | |
| key={item.value} | |
| onPress={() => setSelected(item.value)} | |
| className={chip(selected === item.value)} | |
| {data.map((item) => ( | |
| <TouchableOpacity | |
| key={item.label} | |
| onPress={() => setSelected(item.value)} | |
| className={chip(selected === item.value)} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/`(root)/(tabs)/search/filter-modal/chip.tsx around lines 18 - 22,
Update the key in the data mapping within the chip component to use each
option’s unique label instead of item.value, ensuring the initial null-valued
entries receive non-null React keys while preserving the existing selection and
press behavior.
| const [localMin, setLocalMin] = useState<string>(minPrice ? String(minPrice) : ''); | ||
| const [localMax, setLocalMax] = useState<string>(maxPrice ? String(maxPrice) : ''); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Local price state goes stale when the store changes.
useState initializers run only on the first render. The Modal stays mounted and only toggles visible, so localMin and localMax keep their old text after resetFilters runs or after the price chip is cleared in app/(root)/(tabs)/search.tsx. Reopening the modal then shows prices that are no longer applied. Sync the local state when the modal opens.
🐛 Proposed fix
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
@@
const [localMin, setLocalMin] = useState<string>(minPrice ? String(minPrice) : '');
const [localMax, setLocalMax] = useState<string>(maxPrice ? String(maxPrice) : '');
+
+ useEffect(() => {
+ if (!visible) return;
+ setLocalMin(minPrice !== null ? String(minPrice) : '');
+ setLocalMax(maxPrice !== null ? String(maxPrice) : '');
+ }, [visible, minPrice, maxPrice]);📝 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.
| const [localMin, setLocalMin] = useState<string>(minPrice ? String(minPrice) : ''); | |
| const [localMax, setLocalMax] = useState<string>(maxPrice ? String(maxPrice) : ''); | |
| import { useEffect, useState } from 'react'; | |
| const [localMin, setLocalMin] = useState<string>(minPrice ? String(minPrice) : ''); | |
| const [localMax, setLocalMax] = useState<string>(maxPrice ? String(maxPrice) : ''); | |
| useEffect(() => { | |
| if (!visible) return; | |
| setLocalMin(minPrice !== null ? String(minPrice) : ''); | |
| setLocalMax(maxPrice !== null ? String(maxPrice) : ''); | |
| }, [visible, minPrice, maxPrice]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/`(root)/(tabs)/search/filter-modal/index.tsx around lines 25 - 26,
Synchronize localMin and localMax with minPrice and maxPrice whenever the filter
modal opens, rather than relying only on the useState initializers. Update the
modal’s open/visible handling in the component so reopening after resetFilters
or chip clearing reflects the current store values, while preserving the
existing string formatting and empty-value behavior.
| const handleApply = () => { | ||
| if (localMin) { | ||
| setMinPrice(Number(localMin)); | ||
| } | ||
| if (localMax) { | ||
| setMaxPrice(Number(localMax)); | ||
| } | ||
| onClose(); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Price filters have two commit paths. The modal keeps price values in local input state and in the Zustand store at the same time, and no single path commits them. As a result Apply cannot clear a price, and the presets apply instantly while the text inputs wait for Apply.
components/(root)/(tabs)/search/filter-modal/index.tsx#L47-L55: makehandleApplythe only writer. Parse both fields and write the number ornulltosetMinPriceandsetMaxPrice, and reject non-numeric input.components/(root)/(tabs)/search/filter-modal/price-ranges.tsx#L61-L81: remove thesetMinPriceandsetMaxPricecalls from the preset handler and update onlysetLocalMinandsetLocalMax. DeriveisActivefromlocalMinandlocalMaxso the preset highlight still tracks the pending selection.
📍 Affects 2 files
components/(root)/(tabs)/search/filter-modal/index.tsx#L47-L55(this comment)components/(root)/(tabs)/search/filter-modal/price-ranges.tsx#L61-L81
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/`(root)/(tabs)/search/filter-modal/index.tsx around lines 47 - 55,
Update handleApply in
components/(root)/(tabs)/search/filter-modal/index.tsx:47-55 to parse both local
price fields, write valid numbers or null through setMinPrice and setMaxPrice,
and reject non-numeric input. In
components/(root)/(tabs)/search/filter-modal/price-ranges.tsx:61-81, remove
preset writes to the store, update only setLocalMin and setLocalMax, and derive
isActive from localMin/localMax so Apply is the sole store commit path.
| : <ActivityIndicator | ||
| size={"large"} | ||
| className="text-blue-500" | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Set the indicator color with the color prop.
ActivityIndicator takes its tint from the color prop. A text-blue-500 class does not map to that prop, so the indicator keeps the platform default color.
🐛 Proposed fix
: <ActivityIndicator
size={"large"}
- className="text-blue-500"
+ color="`#3b82f6`"
/>📝 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.
| : <ActivityIndicator | |
| size={"large"} | |
| className="text-blue-500" | |
| /> | |
| : <ActivityIndicator | |
| size={"large"} | |
| color="`#3b82f6`" | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/`(root)/(tabs)/search/list/index.tsx around lines 29 - 32, Update
the ActivityIndicator in the loading state to use its color prop with the
intended blue value, and remove the ineffective text-blue-500 className so the
indicator renders with the specified tint.
… and add map utilities
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/`(root)/(tabs)/index.tsx:
- Around line 69-77: Remove the removeClippedSubviews prop from the FlatList in
the recommended-list component, allowing React Native’s platform defaults to
apply; do not force clipping for the vertical list containing the horizontal
Featured header.
In `@app/`(root)/property/[id].tsx:
- Around line 20-36: Update fetchProperty to wrap the Supabase request in
try/finally, store fetch errors in state instead of returning early, and always
reset loading in finally. In the property screen render flow, initialize loading
as true, show a loading indicator while it is true, and add an effect
cancellation guard so stale responses cannot update the current property.
In `@app/`(root)/property/map.tsx:
- Around line 59-62: Update the TouchableOpacity onPress handler in the map
component to handle rejected Linking.openURL(openMapLink) promises and show a
user-visible fallback when opening the external map fails, including unavailable
applications or dialog cancellation. Preserve the existing open-map behavior for
successful launches.
- Around line 26-29: Validate latitude and longitude before the parseFloat and
URL construction in the map component: require present string values, parse
finite numbers, and enforce latitude -90–90 and longitude -180–180. When
validation fails, render the existing or appropriate unavailable state and do
not call getMapUrl or build openMapLink; only create those URLs after validation
succeeds.
In `@components/`(root)/(tabs)/property/[id]/bottom-buttons.tsx:
- Around line 39-56: Update both property action handlers, including the
sold-update handler and the deletion handler, to show an Alert with the Supabase
error before returning, matching the failure behavior in handleContact. After
successful deletion, replace the current route with router.replace instead of
router.push so the deleted property cannot remain in navigation history.
- Around line 21-25: Update handleContact to construct the WhatsApp URL without
whitespace before ?text= and normalize ADMIN_PHONE to digits only by removing
any leading + or other formatting. Handle the promise returned by
Linking.openURL so failures are caught instead of becoming unhandled rejections.
In `@components/`(root)/(tabs)/property/[id]/image-carousel.tsx:
- Around line 85-91: Update the images prop in the ImageViewing component to
safely handle null property.images by using the same optional guard already
applied nearby, while preserving the existing image mapping behavior when images
are present.
In `@components/`(root)/(tabs)/property/[id]/info/index.tsx:
- Around line 44-46: Update the formatPrice utility in lib/utils.ts to honor its
currency argument for every price range, including values below and at least
1,000,000, instead of hardcoding dollar formatting or omitting the symbol.
Preserve the existing default currency behavior while ensuring the property
detail display through formatPrice(property.price) consistently uses the
requested currency.
In `@components/`(root)/(tabs)/property/[id]/info/property-specs.tsx:
- Around line 19-22: Update the Area value in the property specs component to
include a space between property.area_sqft and the “sqft” unit, so it renders as
“1200 sqft”.
In `@components/`(root)/(tabs)/property/[id]/location.tsx:
- Line 11: Update the getMapUrl helper in constants/index.ts so its URL template
is constructed without literal newlines or indentation, keeping the bbox query
value and resulting OpenStreetMap URL on one logical line. Preserve the existing
longitude, latitude, and padding calculations.
In `@components/`(root)/(tabs)/property/card.tsx:
- Around line 20-21: Update the property card component to use useSavedProperty
for the current property’s isSaved state and toggleSave action instead of
hard-coding isSaved to true. Pass toggleSave to the heart control’s onPress, and
stop event propagation there so pressing the heart does not trigger the
TouchableOpacity card navigation while preserving onUnsave as a callback.
In `@constants/index.ts`:
- Around line 1-5: Update getMapUrl so the returned OpenStreetMap URL is
assembled as one contiguous string without newline or indentation whitespace,
while preserving the existing bbox, layer, and marker parameters and their
values.
In `@hooks/useSavedProperty.ts`:
- Around line 18-33: Update checkIfSaved and its useEffect to reset isSaved to
false before each lookup, use maybeSingle() instead of single() for the
existence check, and guard the awaited state update with a cancellation flag so
stale overlapping requests cannot update state after dependencies change or
unmount.
In `@package.json`:
- Line 41: Replace react-native-image-viewing in the image-viewing integration,
or provide a web-compatible implementation that resolves its ImageItem import
during Expo web export. Ensure the selected solution preserves the existing
native behavior while allowing static web builds to complete.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0d12643d-c1a4-4db7-ab05-7efda967ca5a
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (17)
app/(root)/(tabs)/index.tsxapp/(root)/property/[id].tsxapp/(root)/property/map.tsxcomponents/(root)/(tabs)/featured/card.tsxcomponents/(root)/(tabs)/featured/index.tsxcomponents/(root)/(tabs)/property/[id]/bottom-buttons.tsxcomponents/(root)/(tabs)/property/[id]/image-carousel.tsxcomponents/(root)/(tabs)/property/[id]/info/chip.tsxcomponents/(root)/(tabs)/property/[id]/info/index.tsxcomponents/(root)/(tabs)/property/[id]/info/property-specs.tsxcomponents/(root)/(tabs)/property/[id]/location.tsxcomponents/(root)/(tabs)/property/[id]/nav-buttons.tsxcomponents/(root)/(tabs)/property/card.tsxcomponents/(root)/(tabs)/search/list/index.tsxconstants/index.tshooks/useSavedProperty.tspackage.json
| <FlatList | ||
| data={recommended} | ||
| keyExtractor={(item) => item.id} | ||
| keyExtractor={keyExtractor} | ||
| contentContainerStyle={{ paddingBottom: 100 }} | ||
| showsVerticalScrollIndicator={false} | ||
| initialNumToRender={5} | ||
| maxToRenderPerBatch={5} | ||
| windowSize={5} | ||
| removeClippedSubviews |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target file map ---'
ast-grep outline 'app/(root)/(tabs)/index.tsx' --view expanded
printf '%s\n' '--- target lines ---'
sed -n '1,125p' 'app/(root)/(tabs)/index.tsx'
printf '%s\n' '--- package versions ---'
if [ -f package.json ]; then
node -e 'const p=require("./package.json"); console.log(JSON.stringify({expo:p.dependencies?.expo, reactNative:p.dependencies?.["react-native"], expoRouter:p.dependencies?.["expo-router"]}, null, 2))'
fi
printf '%s\n' '--- relevant list usage ---'
rg -n -C 4 'removeClippedSubviews|FlatList|Featured|horizontal' app components 2>/dev/null || trueRepository: Ijisrael42/kribb
Length of output: 19112
🌐 Web query:
Expo SDK 54 FlatList removeClippedSubviews React Native documentation default Android warning nested horizontal list in ListHeaderComponent
💡 Result:
In React Native (including when using Expo SDK 54), the removeClippedSubviews property is a performance optimization that defaults to true on Android [1][2]. When enabled, off-screen child views are detached from the native view hierarchy to improve scroll performance [2][3]. However, this feature is known to cause bugs, such as missing content, incorrect child counts, or UI glitches—particularly when complex layouts, transforms, or nesting are involved [2][3]. If you encounter issues like disappearing elements or crashes (such as "cannot remove child at index X" on the new architecture), the standard workaround is to set removeClippedSubviews={false} on the affected FlatList [4][5]. While this increases memory usage slightly because items are not detached, it resolves the synchronization issues between the JS and native view counts [5]. Regarding nested lists, the warning "VirtualizedLists should never be nested inside plain ScrollViews with the same orientation" is unrelated to removeClippedSubviews [6]. This warning occurs because nesting two virtualized lists (like a FlatList inside a ScrollView or another FlatList) with the same orientation disrupts the virtualization mechanism [6][7]. To resolve this and avoid the warning, you should use the ListHeaderComponent or ListFooterComponent props of the parent FlatList to place the nested list, rather than wrapping it in a generic ScrollView [7]. Top results: [1][5][2][7]
Citations:
- 1: https://reactnative.dev/docs/flatlist
- 2: https://reactnative.dev/docs/optimizing-flatlist-configuration
- 3: https://reactnative.dev/docs/0.84/flatlist
- 4: fix(Android): incorrect childCount in removeViewAt when using flatlist on fabric software-mansion/react-native-screens#2307
- 5: Android - "cannot remove child at index X" error on new architecture software-mansion/react-native-screens#2339
- 6: "VirtualizedLists should never be nested inside plain ScrollViews with the same orientation" error in console for FlatList/SectionList with scrollEnabled={false} react/react-native#31697
- 7: https://stackoverflow.com/questions/66939975/react-native-virtualizedlists-should-never-be-nested-inside-plain-scrollviews
🌐 Web query:
site:docs.expo.dev/versions/v54.0.0/react-native/flatlist removeClippedSubviews Expo SDK 54
💡 Result:
Expo SDK 54 uses React Native 0.81. The FlatList removeClippedSubviews prop is documented in the React Native component reference, not under Expo’s SDK pages. (docs.expo.dev)
Use:
<FlatList
data={data}
renderItem={renderItem}
removeClippedSubviews={true}
/>It can improve scrolling performance, but may cause missing content in complex layouts; test carefully before enabling it globally.
Citations:
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Featured component ---'
sed -n '1,80p' 'components/(root)/(tabs)/featured/index.tsx'
printf '%s\n' '--- React Native documentation references in repository ---'
rg -n -i 'removeClippedSubviews|virtualized|nested.*list|FlatList' README.md docs app components 2>/dev/null || trueRepository: Ijisrael42/kribb
Length of output: 2673
Avoid forcing removeClippedSubviews on every platform.
React Native 0.81 defaults this prop to true on Android and false on iOS. It can cause missing content in complex layouts. Remove it, or enable it per platform only after testing the vertical list with the horizontal Featured list in its header.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(root)/(tabs)/index.tsx around lines 69 - 77, Remove the
removeClippedSubviews prop from the FlatList in the recommended-list component,
allowing React Native’s platform defaults to apply; do not force clipping for
the vertical list containing the horizontal Featured header.
| const fetchProperty = async () => { | ||
| setLoading(true); | ||
| const { data, error } = await supabase | ||
| .from('properties') | ||
| .select('*') | ||
| .eq('id', id) | ||
| .single(); | ||
|
|
||
| if (error) { | ||
| console.error('Error fetching property:', error); | ||
| return; | ||
| } | ||
| if (data) { | ||
| setProperty(data); | ||
| } | ||
| setLoading(false); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
The loading state is set but never used, so the screen reports "Property not found" during every fetch. The single root cause is an incomplete loading lifecycle: fetchProperty resets the flag only on the success path, and the render path branches only on property.
app/(root)/property/[id].tsx#L20-L36: wrap the Supabase call intry/finally, resetloadinginfinally, and store the error in state instead of returning early.app/(root)/property/[id].tsx#L43-L53: initializeloadingtotrue, render a loading indicator whileloadingistrue, and add a cancellation guard in the effect so a stale response cannot overwrite the current property.
🧰 Tools
🪛 React Doctor (0.9.3)
[error] 35-35: This resets a loading/busy flag only on the success path: if the awaited call rejects the reset never runs and the flag stays stuck truthy (a spinner that never stops, a button disabled forever). Move the reset into a finally block, or mirror it on every catch, so it clears on rejection too.
A trailing setLoading(false) after an await never runs if the awaited call rejects, so the flag stays stuck truthy; reset it in a finally block (or mirror the reset on every catch) so it clears on both paths.
(no-loading-flag-reset-outside-finally)
📍 Affects 1 file
app/(root)/property/[id].tsx#L20-L36(this comment)app/(root)/property/[id].tsx#L43-L53
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(root)/property/[id].tsx around lines 20 - 36, Update fetchProperty to
wrap the Supabase request in try/finally, store fetch errors in state instead of
returning early, and always reset loading in finally. In the property screen
render flow, initialize loading as true, show a loading indicator while it is
true, and add an effect cancellation guard so stale responses cannot update the
current property.
Source: Linters/SAST tools
| const latitudeNumber = parseFloat(latitude!); | ||
| const longitudeNumber = parseFloat(longitude!); | ||
| const mapUrl = getMapUrl(longitudeNumber, latitudeNumber, 0.001); | ||
| const openMapLink = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect every producer of the map route and its coordinate parameters.
rg -n -C 4 -g '*.ts' -g '*.tsx' 'property/map|latitude|longitude' app componentsRepository: Ijisrael42/kribb
Length of output: 3704
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- map route ---'
cat -n 'app/(root)/property/map.tsx'
printf '%s\n' '--- getMapUrl definitions and usages ---'
rg -n -C 5 'function getMapUrl|const getMapUrl|getMapUrl\s*=' --glob '*.{ts,tsx,js,jsx}' .
printf '%s\n' '--- route parameter producers ---'
cat -n 'components/(root)/(tabs)/property/[id]/location.tsx'
rg -n -C 5 'latitude:|longitude:' --glob '*.{ts,tsx,js,jsx}' app components lib hooks store 2>/dev/null || trueRepository: Ijisrael42/kribb
Length of output: 8583
🌐 Web query:
Expo SDK 54 React Native Linking.openURL rejection behavior and useLocalSearchParams parameter types
💡 Result:
In Expo SDK 54, the Linking API and Expo Router hooks function with the following behaviors: Linking.openURL Rejection Behavior The Linking.openURL(url) method returns a Promise [1][2][3]. It rejects in the following scenarios [1][2][3][4]: - No applications are registered on the device to handle the provided URL scheme [2][3][4]. - The user cancels the dialog prompt to open the link [1][2][3]. - On some Android devices, the attempt to open an unsupported or malformed URL may fail if the system cannot resolve an activity to handle the intent [4][5]. It is considered best practice to wrap Linking.openURL in a try/catch block or use a.catch handler to prevent unhandled promise rejections [6][7]. Additionally, always validating the URL via Linking.canOpenURL(url) is recommended before attempting to open it, though be aware that canOpenURL may return false if the necessary schemes are not configured in your Info.plist (iOS) or if specific intent filters are missing (Android) [1][2][4][5]. Expo Router useLocalSearchParams Parameter Types The useLocalSearchParams hook returns an object containing URL parameters (both route parameters and search query parameters) [8][9][10]. Parameter Types: - By default, parameters are returned as strings or arrays of strings (when using rest syntax like [...slug]) [9]. - You can manually type these parameters by passing a generic to the hook [11][9]. - When you need to type both route parameters and query parameters, you can pass two generics to the hook [11]: - The first generic defines the route parameter structure. - The second generic defines the query parameter structure. Example usage: import { useLocalSearchParams } from 'expo-router'; // Typing route and query parameters const { profile, query } = useLocalSearchParams<'/[profile]', { query?: string }>; Note that while route parameters are derived from the file system, query parameters are often arbitrary and should be typed manually to ensure type safety within your components [11][9].
Citations:
- 1: https://docs.expo.dev/versions/v54.0.0/sdk/linking
- 2: https://docs.expo.dev/versions/latest/sdk/linking/
- 3: https://github.com/expo/expo/blob/main/packages/expo-linking/src/Linking.ts
- 4: https://reactnative.dev/docs/linking
- 5: https://stackoverflow.com/questions/72210042/on-react-native-on-android-canopenurl-fails-openurl-throw-an-error-could-no
- 6: https://codingeasypeasy.com/blog/react-native-linking-api-deep-linking-and-opening-urls-effectively/
- 7: Linking.openURL mailto / tel not working in iOS : 'Unable to open URL' react/react-native#37976
- 8: https://docs.expo.dev/versions/v54.0.0/sdk/router
- 9: https://docs.expo.dev/router/reference/url-parameters/
- 10: https://github.com/expo/expo/blob/5cbac55c/packages/expo-router/build/hooks.d.ts
- 11: https://docs.expo.dev/router/reference/typed-routes/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node - <<'JS'
const inputs = [
undefined,
null,
'',
'abc',
'12abc',
'90',
'90.0001',
'-180',
'-180.0001',
['12.3'],
['12.3', '45.6'],
];
for (const value of inputs) {
const latitudeNumber = parseFloat(value);
const longitudeNumber = parseFloat(value);
const mapUrl =
`https://www.openstreetmap.org/export/embed.html?bbox=${longitudeNumber - 0.001}` +
`%2C${latitudeNumber - 0.001}%2C${longitudeNumber + 0.001}%2C${latitudeNumber + 0.001}` +
`&layer=mapnik&marker=${latitudeNumber}%2C${longitudeNumber}`;
const openMapLink =
`https://www.google.com/maps/search/?api=1&query=${value},${value}`;
console.log(JSON.stringify({
input: value,
parsed: latitudeNumber,
finite: Number.isFinite(latitudeNumber),
mapHasNaN: mapUrl.includes('NaN'),
openMapLink,
}));
}
JSRepository: Ijisrael42/kribb
Length of output: 1685
Validate coordinates before building map URLs.
Non-null assertions do not validate runtime URL parameters. Reject missing, non-string, non-numeric, and out-of-range values before creating mapUrl or openMapLink. Render an unavailable state when latitude is outside -90 to 90 or longitude is outside -180 to 180.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(root)/property/map.tsx around lines 26 - 29, Validate latitude and
longitude before the parseFloat and URL construction in the map component:
require present string values, parse finite numbers, and enforce latitude -90–90
and longitude -180–180. When validation fails, render the existing or
appropriate unavailable state and do not call getMapUrl or build openMapLink;
only create those URLs after validation succeeds.
| <TouchableOpacity | ||
| className='flex-row items-center gap-1 px-3 py-2 rounded-full bg-blue-50' | ||
| onPress={() => Linking.openURL(openMapLink)} | ||
| > |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
fd -t f -i 'map\.tsx$|package\.json$|app\.json$|app\.config\.(js|ts)$' .
printf '%s\n' '--- route structure ---'
ast-grep outline 'app/(root)/property/map.tsx' --lang tsx
printf '%s\n' '--- relevant source ---'
sed -n '1,120p' 'app/(root)/property/map.tsx'
printf '%s\n' '--- Linking usage ---'
rg -n -C 3 'Linking\.openURL|openMapLink|latitude|longitude' 'app/(root)/property/map.tsx' .Repository: Ijisrael42/kribb
Length of output: 9936
🌐 Web query:
Expo SDK 54 React Native Linking.openURL rejects no app can open URL cancellation documentation
💡 Result:
In Expo SDK 54, the Linking.openURL method is designed to return a Promise that rejects specifically when no applications are registered to handle the provided URL or when the user manually cancels the opening dialog [1][2][3]. Documentation for Expo Linking explicitly states this behavior [1][2]. Because these scenarios trigger a promise rejection, it is considered a best practice to wrap your Linking.openURL calls in a try-catch block to handle these cases gracefully in your application code [4]. Regarding the specific behaviors: 1. Rejection on Cancellation: If the operating system presents an "open with" dialog and the user chooses to cancel, the promise will reject [1][3]. 2. Rejection when No App is Found: If the system cannot locate an application capable of opening the URL, the promise will likewise reject [1][3]. It is also recommended to use Linking.canOpenURL before calling Linking.openURL to verify that a URL can be handled [4]. Note that Linking.canOpenURL itself has specific conditions where it may reject—such as on Android if the check cannot be performed, or on iOS if the required URL scheme is not properly configured in your Info.plist's LSApplicationQueriesSchemes key [1][2][5]. For web platforms, Linking.canOpenURL always resolves to true as there is no universal API for detecting installed app capabilities [1][2].
Citations:
- 1: https://docs.expo.dev/versions/v54.0.0/sdk/linking
- 2: https://docs.expo.dev/versions/latest/sdk/linking/
- 3: https://github.com/expo/expo/blob/main/packages/expo-linking/src/Linking.ts
- 4: https://mintlify.wiki/facebook/react-native/apis/linking
- 5: https://reactnative.dev/docs/linking
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- dependency versions ---'
cat package.json
printf '%s\n' '--- Expo SDK 54 Linking documentation ---'
curl -LfsS 'https://docs.expo.dev/versions/v54.0.0/sdk/linking/' |
rg -o -i '.{0,180}(openURL|reject|cancel).{0,260}' | head -20
printf '%s\n' '--- React Native Linking documentation ---'
curl -LfsS 'https://reactnative.dev/docs/linking' |
rg -o -i '.{0,180}(openURL|reject|cancel).{0,260}' | head -20Repository: Ijisrael42/kribb
Length of output: 13949
Handle failure when opening the external map.
Line 61 leaves the Linking.openURL promise unhandled. Catch rejected launches and show a user-visible fallback when no application can open the URL or the user cancels the dialog.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(root)/property/map.tsx around lines 59 - 62, Update the
TouchableOpacity onPress handler in the map component to handle rejected
Linking.openURL(openMapLink) promises and show a user-visible fallback when
opening the external map fails, including unavailable applications or dialog
cancellation. Preserve the existing open-map behavior for successful launches.
| const handleContact = () => { | ||
| const message = `Hi! I'm interested in the property: ${property?.title}`; | ||
| const url = `https://wa.me/${ADMIN_PHONE} ?text=${encodeURIComponent(message)}`; | ||
| Linking.openURL(url); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
The WhatsApp URL is malformed and openURL can reject.
Line 23 contains a space before ?text=, so the phone segment becomes +27763644146%20. wa.me also expects digits only, without the + prefix. The link therefore fails to open the chat.
Linking.openURL rejects when no handler exists for the URL. That produces an unhandled rejection.
🐛 Proposed fix
-const ADMIN_PHONE = "+27763644146"
+const ADMIN_PHONE = "27763644146"
@@
- const handleContact = () => {
+ const handleContact = async () => {
const message = `Hi! I'm interested in the property: ${property?.title}`;
- const url = `https://wa.me/${ADMIN_PHONE} ?text=${encodeURIComponent(message)}`;
- Linking.openURL(url);
+ const url = `https://wa.me/${ADMIN_PHONE}?text=${encodeURIComponent(message)}`;
+ try {
+ await Linking.openURL(url);
+ } catch {
+ Alert.alert('Error', 'Unable to open WhatsApp.');
+ }
}📝 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.
| const handleContact = () => { | |
| const message = `Hi! I'm interested in the property: ${property?.title}`; | |
| const url = `https://wa.me/${ADMIN_PHONE} ?text=${encodeURIComponent(message)}`; | |
| Linking.openURL(url); | |
| } | |
| const handleContact = async () => { | |
| const message = `Hi! I'm interested in the property: ${property?.title}`; | |
| const url = `https://wa.me/${ADMIN_PHONE}?text=${encodeURIComponent(message)}`; | |
| try { | |
| await Linking.openURL(url); | |
| } catch { | |
| Alert.alert('Error', 'Unable to open WhatsApp.'); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/`(root)/(tabs)/property/[id]/bottom-buttons.tsx around lines 21 -
25, Update handleContact to construct the WhatsApp URL without whitespace before
?text= and normalize ADMIN_PHONE to digits only by removing any leading + or
other formatting. Handle the promise returned by Linking.openURL so failures are
caught instead of becoming unhandled rejections.
|
|
||
| const Location = ({ property }: { property: Property }) => { | ||
| const router = useRouter(); | ||
| const mapUrl = getMapUrl(property.longitude, property.latitude, 0.003); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
getMapUrl returns a URL that contains newlines and spaces.
The helper in constants/index.ts (Lines 1-5) builds the URL with a multi-line template literal. The line breaks and indentation become literal characters inside the bbox query value, so the embedded OpenStreetMap request is malformed.
Fix the helper so the template stays on one logical line:
-export const getMapUrl = (longitude: number, latitude: number, zoom: number) => {
- return `https://www.openstreetmap.org/export/embed.html?bbox=${longitude - zoom}
- %2C${latitude - zoom}%2C${longitude + zoom}%2C${latitude + zoom}
- &layer=mapnik&marker=${latitude}%2C${longitude}`;
-}
+export const getMapUrl = (longitude: number, latitude: number, zoom: number) => {
+ const bbox = [longitude - zoom, latitude - zoom, longitude + zoom, latitude + zoom].join('%2C');
+ return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox}&layer=mapnik&marker=${latitude}%2C${longitude}`;
+}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/`(root)/(tabs)/property/[id]/location.tsx at line 11, Update the
getMapUrl helper in constants/index.ts so its URL template is constructed
without literal newlines or indentation, keeping the bbox query value and
resulting OpenStreetMap URL on one logical line. Preserve the existing
longitude, latitude, and padding calculations.
| <TouchableOpacity | ||
| onPress={() => router.push(`/property/${property.id}`)} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect every PropertyCard save-action caller and implementation.
rg -n -C 4 -g '*.ts' -g '*.tsx' '\bonUnsave\b|\bshowSave\b|<PropertyCard\b' app componentsRepository: Ijisrael42/kribb
Length of output: 2427
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- card implementation ---'
cat -n 'components/(root)/(tabs)/property/card.tsx'
printf '%s\n' '--- PropertyCard references and save-related symbols ---'
rg -n -C 5 '\bPropertyCard\b|\bonUnsave\b|\bshowSave\b|\bisSaved\b' --glob '*.ts' --glob '*.tsx' .
printf '%s\n' '--- package and Expo SDK declarations ---'
rg -n '"expo"|"react-native"' package.json package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null || trueRepository: Ijisrael42/kribb
Length of output: 19408
🌐 Web query:
React Native official documentation nested TouchableOpacity child parent onPress responder press event propagation
💡 Result:
In React Native, nested TouchableOpacity components do not follow standard web-based event propagation (bubbling) where an event on a child automatically triggers the parent's handler [1][2][3]. Instead, the React Native Gesture Responder System uses a negotiation process where the deepest component in the view hierarchy typically claims the touch responder status, effectively consuming the event and preventing it from bubbling up to parent components [2][4][3]. Key points regarding nested TouchableOpacity components include: 1. Responder System Behavior: The system is designed such that the deepest node that returns true for responder negotiation handlers (like onStartShouldSetResponder) becomes the responder [2][3]. Consequently, when a child TouchableOpacity is pressed, it typically consumes the touch event, preventing the parent from receiving it [1][5][6]. 2. No Default Propagation: Unlike the DOM, where events bubble up through the hierarchy by default, React Native does not bubble onPress events up to parent touchable components [1][7]. 3. Common Workarounds: - Manual Invocation: If you need both handlers to execute, the standard approach is to manually call the parent's function from within the child's onPress handler [1]. - Event Stopping: If you encounter unwanted triggering of a parent (common in some third-party libraries or specific configurations), you can use e.stopPropagation within the child's onPress handler to explicitly prevent the event from propagating further if the library supports it [5]. - Component Selection: For complex interactions, consider using the Pressable component, which provides more fine-grained control over touch state and interactions compared to the legacy TouchableOpacity [8]. 4. Third-Party Libraries: If you are using React Native Gesture Handler, be aware that its components may behave differently than the built-in React Native components [9][10][11]. Issues where both parent and child handlers fire or where children become unresponsive often arise from how these libraries implement their own responder logic; checking the specific documentation and versioning for those libraries is recommended [10][12][6].
Citations:
- 1: https://stackoverflow.com/questions/54362152/how-to-bubbling-press-event-from-inner-touchable-to-outer-touchable-in-react-nat
- 2: https://reactnative.dev/docs/next/gesture-responder-system
- 3: https://reactnative.dev/docs/gesture-responder-system
- 4: https://reactnative.dev/docs/0.83/gesture-responder-system
- 5: https://stackoverflow.com/questions/44593024/stacked-touchableopacity-inside-another-touchableopacity-is-not-clickable
- 6: onPress action on Pressable component inside another triggers both actions software-mansion/react-native-gesture-handler#3282
- 7: https://stackoverflow.com/questions/50828717/nested-touchables-in-react-native-event-propagation-not-working
- 8: https://reactnative.dev/docs/pressable
- 9: https://stackoverflow.com/questions/61995709/how-to-use-nested-touchables-with-react-native-gesture-handler
- 10: Nested Touchables activate parent Touchable and break Flexbox software-mansion/react-native-gesture-handler#1824
- 11: [TouchableOpacity]: Nested TouchableOpacity components seem to be "under" the parent TouchableOpacity software-mansion/react-native-gesture-handler#2377
- 12: https://docs.swmansion.com/react-native-gesture-handler/docs/components/touchable/
Connect the heart control to the save action.
isSaved is hard-coded to true, and the heart control has no onPress. Use useSavedProperty to provide isSaved and toggleSave; onUnsave is only a callback. Isolate the heart press from card navigation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/`(root)/(tabs)/property/card.tsx around lines 20 - 21, Update the
property card component to use useSavedProperty for the current property’s
isSaved state and toggleSave action instead of hard-coding isSaved to true. Pass
toggleSave to the heart control’s onPress, and stop event propagation there so
pressing the heart does not trigger the TouchableOpacity card navigation while
preserving onUnsave as a callback.
| export const getMapUrl = (longitude: number, latitude: number, zoom: number) => { | ||
| return `https://www.openstreetmap.org/export/embed.html?bbox=${longitude - zoom} | ||
| %2C${latitude - zoom}%2C${longitude + zoom}%2C${latitude + zoom} | ||
| &layer=mapnik&marker=${latitude}%2C${longitude}`; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Generate a contiguous map URL.
Lines 2-4 insert newline and indentation characters into the returned URL. The bbox and layer query values therefore contain whitespace before the WebView receives them. Build the query string without embedded whitespace.
Proposed fix
export const getMapUrl = (longitude: number, latitude: number, zoom: number) => {
- return `https://www.openstreetmap.org/export/embed.html?bbox=${longitude - zoom}
- %2C${latitude - zoom}%2C${longitude + zoom}%2C${latitude + zoom}
- &layer=mapnik&marker=${latitude}%2C${longitude}`;
+ return `https://www.openstreetmap.org/export/embed.html?bbox=${longitude - zoom}%2C${latitude - zoom}%2C${longitude + zoom}%2C${latitude + zoom}&layer=mapnik&marker=${latitude}%2C${longitude}`;
}📝 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.
| export const getMapUrl = (longitude: number, latitude: number, zoom: number) => { | |
| return `https://www.openstreetmap.org/export/embed.html?bbox=${longitude - zoom} | |
| %2C${latitude - zoom}%2C${longitude + zoom}%2C${latitude + zoom} | |
| &layer=mapnik&marker=${latitude}%2C${longitude}`; | |
| } | |
| export const getMapUrl = (longitude: number, latitude: number, zoom: number) => { | |
| return `https://www.openstreetmap.org/export/embed.html?bbox=${longitude - zoom}%2C${latitude - zoom}%2C${longitude + zoom}%2C${latitude + zoom}&layer=mapnik&marker=${latitude}%2C${longitude}`; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@constants/index.ts` around lines 1 - 5, Update getMapUrl so the returned
OpenStreetMap URL is assembled as one contiguous string without newline or
indentation whitespace, while preserving the existing bbox, layer, and marker
parameters and their values.
| const checkIfSaved = async () => { | ||
| if (!userId) return; | ||
|
|
||
| const { data, error } = await authSupabase | ||
| .from('saved_properties') | ||
| .select('id') | ||
| .eq('user_clerk_id', userId) | ||
| .eq('property_id', propertyId) | ||
| .single(); | ||
|
|
||
| if (data) setIsSaved(true); | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| checkIfSaved(); | ||
| }, [propertyId, userId]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reset isSaved and guard the async effect.
checkIfSaved only sets isSaved to true. When propertyId or userId changes, a previous true value persists. The heart icon then shows the wrong state, and toggleSave runs a delete instead of an insert.
The effect also writes state after await with no cancellation flag, so overlapping runs can resolve out of order.
.single() returns an error when no row matches. .maybeSingle() returns null data without an error for this check.
🐛 Proposed fix
- const checkIfSaved = async () => {
- if (!userId) return;
-
- const { data, error } = await authSupabase
- .from('saved_properties')
- .select('id')
- .eq('user_clerk_id', userId)
- .eq('property_id', propertyId)
- .single();
-
- if (data) setIsSaved(true);
- }
-
- useEffect(() => {
- checkIfSaved();
- }, [propertyId, userId]);
+ useEffect(() => {
+ let cancelled = false;
+
+ const checkIfSaved = async () => {
+ if (!userId) {
+ setIsSaved(false);
+ return;
+ }
+
+ const { data, error } = await authSupabase
+ .from('saved_properties')
+ .select('id')
+ .eq('user_clerk_id', userId)
+ .eq('property_id', propertyId)
+ .maybeSingle();
+
+ if (cancelled) return;
+ if (error) {
+ console.error('Error checking saved property:', error.message);
+ return;
+ }
+ setIsSaved(!!data);
+ }
+
+ checkIfSaved();
+ return () => { cancelled = true; };
+ }, [propertyId, userId]);📝 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.
| const checkIfSaved = async () => { | |
| if (!userId) return; | |
| const { data, error } = await authSupabase | |
| .from('saved_properties') | |
| .select('id') | |
| .eq('user_clerk_id', userId) | |
| .eq('property_id', propertyId) | |
| .single(); | |
| if (data) setIsSaved(true); | |
| } | |
| useEffect(() => { | |
| checkIfSaved(); | |
| }, [propertyId, userId]); | |
| useEffect(() => { | |
| let cancelled = false; | |
| const checkIfSaved = async () => { | |
| if (!userId) { | |
| setIsSaved(false); | |
| return; | |
| } | |
| const { data, error } = await authSupabase | |
| .from('saved_properties') | |
| .select('id') | |
| .eq('user_clerk_id', userId) | |
| .eq('property_id', propertyId) | |
| .maybeSingle(); | |
| if (cancelled) return; | |
| if (error) { | |
| console.error('Error checking saved property:', error.message); | |
| return; | |
| } | |
| setIsSaved(!!data); | |
| } | |
| checkIfSaved(); | |
| return () => { cancelled = true; }; | |
| }, [propertyId, userId]); |
🧰 Tools
🪛 React Doctor (0.9.3)
[error] 31-31: This setter runs after await, so overlapping re-runs of the effect can resolve out of order and write stale state; gate it behind a cancellation/ignore flag or return a cleanup that cancels the work.
In a useEffect whose dependencies can change, guard any setter call that runs after an await behind a cancellation/ignore flag, or return a cleanup that cancels the async work.
(no-set-state-after-await-in-effect)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hooks/useSavedProperty.ts` around lines 18 - 33, Update checkIfSaved and its
useEffect to reset isSaved to false before each lookup, use maybeSingle()
instead of single() for the existence check, and guard the awaited state update
with a cancellation flag so stale overlapping requests cannot update state after
dependencies change or unmount.
Source: Linters/SAST tools
| "react-native": "0.81.5", | ||
| "react-native-css": "^3.0.7", | ||
| "react-native-gesture-handler": "~2.28.0", | ||
| "react-native-image-viewing": "^0.2.2", |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- package.json ---'
cat -n package.json | sed -n '30,55p'
printf '%s\n' '--- image viewer references ---'
rg -n -C 8 'react-native-image-viewing|ImageItem|image-carousel' --glob '!node_modules' .
printf '%s\n' '--- platform-specific files near image carousel ---'
fd -i 'image-carousel|ImageItem' . --type fRepository: Ijisrael42/kribb
Length of output: 10153
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- image-carousel.tsx ---'
cat -n 'components/(root)/(tabs)/property/[id]/image-carousel.tsx' | sed -n '1,140p'
printf '%s\n' '--- published package metadata and files ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL 'https://registry.npmjs.org/react-native-image-viewing/0.2.2' -o "$tmpdir/metadata.json"
python3 - "$tmpdir/metadata.json" <<'PY'
import json, sys
data = json.load(open(sys.argv[1]))
print(json.dumps({
"name": data.get("name"),
"version": data.get("version"),
"main": data.get("main"),
"module": data.get("module"),
"browser": data.get("browser"),
"react-native": data.get("react-native"),
"files": data.get("files"),
"repository": data.get("repository"),
}, indent=2))
print("tarball:", data["dist"]["tarball"])
PY
tarball="$(python3 - "$tmpdir/metadata.json" <<'PY'
import json, sys
print(json.load(open(sys.argv[1]))["dist"]["tarball"])
PY
)"
curl -fsSL "$tarball" -o "$tmpdir/package.tgz"
tar -tzf "$tmpdir/package.tgz" | sed -n '1,120p'Repository: Ijisrael42/kribb
Length of output: 6007
🏁 Script executed:
#!/bin/bash
set -e
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL 'https://registry.npmjs.org/react-native-image-viewing/0.2.2' -o "$tmpdir/metadata.json"
tarball="$(python3 - "$tmpdir/metadata.json" <<'PY'
import json, sys
print(json.load(open(sys.argv[1]))["dist"]["tarball"])
PY
)"
curl -fsSL "$tarball" -o "$tmpdir/package.tgz"
mkdir "$tmpdir/pkg"
tar -xzf "$tmpdir/package.tgz" -C "$tmpdir/pkg"
printf '%s\n' '--- package.json ---'
cat "$tmpdir/pkg/package/package.json"
printf '%s\n' '--- entry-point imports ---'
for file in \
"$tmpdir/pkg/package/dist/index.js" \
"$tmpdir/pkg/package/dist/ImageViewing.js" \
"$tmpdir/pkg/package/dist/components/Modal/Modal.js" \
"$tmpdir/pkg/package/dist/components/ImageItem/ImageItem.android.js" \
"$tmpdir/pkg/package/dist/components/ImageItem/ImageItem.ios.js"
do
echo "--- ${file##*/} ---"
cat "$file"
done
printf '%s\n' '--- web configuration ---'
find . -maxdepth 2 -type f \( -name 'metro.config.*' -o -name 'app.json' -o -name 'app.config.*' -o -name 'package.json' \) -print
rg -n -C 5 '"web"|platforms|resolver|react-native-web|expo export|expo start' \
package.json app.json app.config.* metro.config.* 2>/dev/null || true
printf '%s\n' '--- upstream issue references ---'
curl -fsSL 'https://api.github.com/repos/jobtoday/react-native-image-viewing/issues?state=all&per_page=100' |
python3 -c 'import json,sys; issues=json.load(sys.stdin); [print(i["number"], i["state"], i["title"], i["html_url"]) for i in issues if "web" in i["title"].lower() or "web" in i.get("body","").lower()]'Repository: Ijisrael42/kribb
Length of output: 16980
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- Metro configuration ---'
cat -n metro.config.js
printf '%s\n' '--- web-support issue ---'
for n in 232 222 221; do
curl -fsSL "https://api.github.com/repos/jobtoday/react-native-image-viewing/issues/$n" |
python3 -c '
import json, sys
x=json.load(sys.stdin)
print(json.dumps({
"number": x.get("number"),
"state": x.get("state"),
"title": x.get("title"),
"html_url": x.get("html_url"),
"body": x.get("body"),
}, indent=2))
'
done
printf '%s\n' '--- route and web output configuration ---'
cat -n app.json | sed -n '1,40p'
cat -n 'app/(root)/property/[id].tsx' | sed -n '1,35p'Repository: Ijisrael42/kribb
Length of output: 4153
Add a web implementation or replace react-native-image-viewing before merging. react-native-image-viewing@0.2.2 imports ImageItem, but ships only Android and iOS implementations. The static Expo web export cannot resolve this import.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@package.json` at line 41, Replace react-native-image-viewing in the
image-viewing integration, or provide a web-compatible implementation that
resolves its ImageItem import during Expo web export. Ensure the selected
solution preserves the existing native behavior while allowing static web builds
to complete.
Summary by CodeRabbit