Conversation
) Bumps the npm-runtime-patch-minor group with 2 updates: [@supabase/supabase-js](https://github.com/supabase/supabase-js/tree/HEAD/packages/core/supabase-js) and [posthog-react-native](https://github.com/PostHog/posthog-js/tree/HEAD/packages/react-native). Updates `@supabase/supabase-js` from 2.110.8 to 2.111.0 - [Release notes](https://github.com/supabase/supabase-js/releases) - [Changelog](https://github.com/supabase/supabase-js/blob/master/packages/core/supabase-js/CHANGELOG.md) - [Commits](https://github.com/supabase/supabase-js/commits/v2.111.0/packages/core/supabase-js) Updates `posthog-react-native` from 4.61.0 to 4.61.1 - [Release notes](https://github.com/PostHog/posthog-js/releases) - [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/react-native/CHANGELOG.md) - [Commits](https://github.com/PostHog/posthog-js/commits/posthog-react-native@4.61.1/packages/react-native) --- updated-dependencies: - dependency-name: "@supabase/supabase-js" dependency-version: 2.111.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-runtime-patch-minor - dependency-name: posthog-react-native dependency-version: 4.61.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-runtime-patch-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Javier Peña Gómez <60773775+Japegomez@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.4 to 3.1.5. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](fastify/fast-uri@v3.1.4...v3.1.5) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Javier Peña Gómez <60773775+Japegomez@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [undici](https://github.com/nodejs/undici) from 6.27.0 to 6.28.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](nodejs/undici@v6.27.0...v6.28.0) --- updated-dependencies: - dependency-name: undici dependency-version: 6.28.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Javier Peña Gómez <60773775+Japegomez@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Route version updates to develop with prod/dev/security groups, cooldown, and Expo-safe ignores; document reporting policy and maintenance tasks. Co-authored-by: Cursor <cursoragent@cursor.com>
Bump overrides (undici, fast-uri, nanoid, js-yaml, postcss) and vendor brace-expansion 5.0.9; allowlist unpatched image-size GHSAs in CI until Expo upgrades Metro. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
… podium (#142) * feat(stats): add player statistics, ELO ranking, and H2H match insights Introduce player_stats with ELO, win rate, rivalries, and badges backed by Supabase RPCs, plus profile stats screens, match H2H context, and an ELO leaderboard. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(stats): add tournament podium medals with named lists Show gold, silver and bronze medal counts on profile stats and list each tournament title in the stats detail screen. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(stats): unify league/tournament podiums and improve profile stats UX Merge podium medals across tournaments and leagues with source labels in detail view, surface win rate and medals at the top of profiles, and fix get_player_stats RPC volatility for PostgREST. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(matches): include phone_e164 in nested profile fallback select Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Satisfy CodeQL actions/missing-workflow-permissions by declaring least-privilege permissions on CI, Quality, Release, and Sentry health workflows. Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(leagues): add league format with round-robin, Elo, and UX polish Introduce leagues end-to-end (schema, services, screens, explore integration) with pair management, standings, and match grouping by jornadas. Includes profile visibility fixes, auto-finish when all fixtures complete, result screen name resolution, icon-based pair actions, and navigation from matches to their league. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(nav): return to match from league and to profile from history Preserve origin when closing match/league/tournament screens with X so league detail returns to the match and profile history returns to the profile. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(profile): polish stats UX, badge showcase, ranking and admin access Improve profile statistics with visual podium, featured badges, unlock popup, city/global ranking, safer account deletion, history-based navigation, and admin bypass for private password-protected events. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(stats): refresh ELO on profile load and fix match timers Keep league fixtures planned until played, extend tournament no-result timeout to 24h, and recalculate player stats when opening the profile. Also polish add-pair UX, ELO help copy, and empty podium messaging. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(deps-dev): bump the npm-dev-patch-minor group with 2 updates Bumps the npm-dev-patch-minor group with 2 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) and [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser). Updates `@typescript-eslint/eslint-plugin` from 8.65.0 to 8.66.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.65.0 to 8.66.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.66.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.66.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-dev-patch-minor - dependency-name: "@typescript-eslint/parser" dependency-version: 8.66.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-dev-patch-minor ... Signed-off-by: dependabot[bot] <support@github.com> * chore(deps): bump the npm-runtime-patch-minor group with 4 updates Bumps the npm-runtime-patch-minor group with 4 updates: [@hookform/resolvers](https://github.com/react-hook-form/resolvers), [@supabase/supabase-js](https://github.com/supabase/supabase-js/tree/HEAD/packages/core/supabase-js), [posthog-react-native](https://github.com/PostHog/posthog-js/tree/HEAD/packages/react-native) and [react-hook-form](https://github.com/react-hook-form/react-hook-form). Updates `@hookform/resolvers` from 5.5.7 to 5.7.1 - [Release notes](https://github.com/react-hook-form/resolvers/releases) - [Commits](react-hook-form/resolvers@v5.5.7...v5.7.1) Updates `@supabase/supabase-js` from 2.110.8 to 2.112.2 - [Release notes](https://github.com/supabase/supabase-js/releases) - [Changelog](https://github.com/supabase/supabase-js/blob/master/packages/core/supabase-js/CHANGELOG.md) - [Commits](https://github.com/supabase/supabase-js/commits/v2.112.2/packages/core/supabase-js) Updates `posthog-react-native` from 4.61.0 to 4.62.0 - [Release notes](https://github.com/PostHog/posthog-js/releases) - [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/react-native/CHANGELOG.md) - [Commits](https://github.com/PostHog/posthog-js/commits/posthog-react-native@4.62.0/packages/react-native) Updates `react-hook-form` from 7.83.0 to 7.84.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](react-hook-form/react-hook-form@v7.83.0...v7.84.0) --- updated-dependencies: - dependency-name: "@hookform/resolvers" dependency-version: 5.7.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-runtime-patch-minor - dependency-name: "@supabase/supabase-js" dependency-version: 2.112.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-runtime-patch-minor - dependency-name: posthog-react-native dependency-version: 4.62.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-runtime-patch-minor - dependency-name: react-hook-form dependency-version: 7.84.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-runtime-patch-minor ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
REVOKE process_league_lifecycle and enqueue RPCs; fix migration 104 typo; refresh player stats only for self/admin. Docs updated for sync and hardening. Co-authored-by: Cursor <cursoragent@cursor.com>
Also make cron.unschedule idempotent on first apply of the player stats queue migration. Co-authored-by: Cursor <cursoragent@cursor.com>
Align REQUIREMENTS, TASKS and README with Fase 6 (ligas, ELO/badges/leaderboard, contactos) and migration 105 anonymization. Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(friends): friendships table, RPCs, services/hooks and profile UI
- Migration 108: friendships table + RLS + send/respond/cancel/list RPCs
- Migration 109: match_invitations + invite/respond/cancel RPCs; pending
invitations count as roster slots; rival validation includes pending/accepted
invites; reject cancels started/finished matches; trigger auto-cancel on match
cancel; delete_user_account_data cleans friendships + invitations
- database.types.ts: friendships + match_invitations tables and new RPCs
- services/hooks: friends.service, matchInvitations.service, useFriends,
useMatchInvitations, PostHog events
- Profile (own): collapsible FriendsSection (friends, received/sent requests)
- Profile (other): person+plus friend button with 4 states + send-request modal
* feat(matches): invitations section in Mis Partidas + invite banner in ficha
- Mis Partidas: new 'Invitaciones' section with inline Accept/Reject and
deep-link into the match ficha on accept
- Match ficha: invitation banner ('X te ha invitado ... a su equipo / al
equipo rival') with Accept/Reject; accepting a pending_validation rival
invite auto-opens the result confirmation modal
- ?confirmResult=1 deep-link param auto-opens the result confirmation modal
for rival-team viewers
* feat(create-edit): invite friends when creating a match and from EditMatchTeamModal
- Create match: friends picker to invite as partner (team A) or rival (team B);
after creation, invites are sent and a WhatsApp deeplink to the ficha is
shared. Past matches with rival invites create a pending_validation result
(via submit_match_result) instead of a directly-confirmed one.
- EditMatchTeamModal: new 'Añadir amigo' tab listing friends with an Invitar
button that sends the invitation and shares the WhatsApp deeplink.
* test(friends): unit tests for friends/matchInvitations services and analytics events
* feat(friends): remove friend + notification preferences for friend/match-invite alerts
- Migration 110: remove_friend(p_other_user_id) RPC (either party deletes an
accepted friendship) + notify_on_friend_request / notify_on_match_invitation
profile columns, wired into enqueue_notification (CASE branches) and the
process-notifications Edge Function (isNotificationAllowed). GRANTs updated.
- Client: database.types.ts (profile columns + remove_friend RPC),
profiles.service (ProfileRow/Update/select), notificationPrefs helper + the
two new toggles in the profile screen ('Solicitudes de amistad',
'Invitaciones a partidas').
- friends.service/hook: removeFriend + useRemoveFriend.
- FriendsSection: trash action on accepted friends with confirm prompt.
- Tests: removeFriend + new preference toggles.
* chore(release): bump version to 1.8.0 and document friends/match invites
- app.json / package.json / package-lock → 1.8.0
- TASKS.md: Release 1.8 checklist (amigos, invitaciones, prefs notif.)
- REQUIREMENTS.md: v1.8 features, F2/F4/F7, CA_PROF8–9, prefs and PostHog
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Warning Review limit reached
Next review available in: 18 seconds You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: QUIET Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (36)
📝 WalkthroughWalkthroughSe incorporan amistades e invitaciones a partidas con soporte en Supabase, hooks, servicios y pantallas. También se añaden preferencias de notificación, analítica, fixtures de ligas, controles de seguridad, documentación y la versión 1.8.0. ChangesAmistades e invitaciones
Seguridad, ligas y release
Estimated code review effort: 5 (Critical) | ~120 minutos Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 3📝 Generate docstrings 💡
⚔️ Resolve merge conflicts 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.
🟡 Other comments (12)
supabase/migrations/20260812100000_108_friendships.sql-80-88 (1)
80-88: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winNotifique la aceptación automática al solicitante original.
Si la otra parte ya tenía una solicitud pendiente, la función acepta la amistad y retorna en la línea 86. El
PERFORM public.enqueue_notificationde la línea 107 no se ejecuta. El solicitante original no recibe ningún aviso de que ya son amigos.Encole una notificación de aceptación antes del
RETURN.🤖 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 `@supabase/migrations/20260812100000_108_friendships.sql` around lines 80 - 88, En el bloque de estado pendiente de la función que actualiza public.friendships, encole una notificación de aceptación mediante public.enqueue_notification antes de retornar v_row.id cuando se realiza la aceptación automática. Use los identificadores y destinatarios del registro existente para notificar al solicitante original, manteniendo intactos la actualización y el flujo de error restante.supabase/migrations/20260812100000_108_friendships.sql-288-288 (1)
288-288: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winHaga idempotente el alta en la publicación de realtime.
ALTER PUBLICATION supabase_realtime ADD TABLEfalla si la tabla ya pertenece a la publicación. El resto del archivo usaIF NOT EXISTS, por lo que una reejecución de la migración aborta solo en esta línea.♻️ Corrección propuesta
-ALTER PUBLICATION supabase_realtime ADD TABLE public.friendships; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_publication_tables + WHERE pubname = 'supabase_realtime' + AND schemaname = 'public' + AND tablename = 'friendships' + ) THEN + ALTER PUBLICATION supabase_realtime ADD TABLE public.friendships; + END IF; +END +$$;🤖 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 `@supabase/migrations/20260812100000_108_friendships.sql` at line 288, Make the ALTER PUBLICATION statement for public.friendships idempotent by using the supported conditional form, so rerunning the migration succeeds when the table is already part of supabase_realtime.src/app/(tabs)/profile/[userId].tsx-251-257 (1)
251-257: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAñada
accessibilityRoleal indicador de amistad aceptada.El
Viewsolo aportaaccessibilityLabel. SinaccessibleniaccessibilityRole, los lectores de pantalla no anuncian el estado de forma fiable.- <View style={styles.friendBtn} accessibilityLabel="Sois amigos"> + <View style={styles.friendBtn} accessible accessibilityRole="image" accessibilityLabel="Sois amigos">🤖 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 `@src/app/`(tabs)/profile/[userId].tsx around lines 251 - 257, Update the accepted-friend indicator View in the status === 'accepted' branch to be an accessible element: enable accessibility and assign the appropriate accessibilityRole alongside the existing accessibilityLabel, so screen readers reliably announce the accepted friendship state.src/components/profile/FriendsSection.tsx-201-240 (1)
201-240: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAísla el estado de carga por solicitud.
respond.isPendingycancel.isPendingpertenecen a una mutación compartida por cadaRequestsList. Por esoButtonmuestra el indicador y se deshabilita en todas las filas. Guarda losfriendship_iden curso y pasaloadingsolo a la fila correspondiente. No uses solorespond.variablesocancel.variables: TanStack Query conserva únicamente las variables de la llamada más reciente.🤖 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 `@src/components/profile/FriendsSection.tsx` around lines 201 - 240, Update FriendsSection’s request action state to track in-flight friendship_id values per mutation, rather than relying on shared respond.isPending or cancel.isPending. Use the tracked IDs to set loading only on the matching row’s accept/reject or cancel Button, and remove each ID when its mutation settles; do not derive this from respond.variables or cancel.variables.REQUIREMENTS.md-173-174 (1)
173-174: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winDefine un único contrato de visibilidad para Descubrir.
La documentación no coincide sobre la visibilidad de entidades privadas con contraseña.
REQUIREMENTS.md#L173-L174: indica si Descubrir incluye entidades privadas y documenta el requisito de contraseña.REQUIREMENTS.md#L249-L250: alinea el listado de ligas privadas con esa decisión.README.md#L188-L189: usa la misma definición de visibilidad queREQUIREMENTS.md.🤖 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 `@REQUIREMENTS.md` around lines 173 - 174, Define one consistent visibility contract for Descubrir across REQUIREMENTS.md lines 173-174 and 249-250, and README.md lines 188-189: explicitly state whether private password-protected entities appear in discovery and document the password requirement consistently in all three locations, including the private-league listing.REQUIREMENTS.md-731-731 (1)
731-731: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winIncluye el fallback web en
CA_CONTACT1.Line 122 exige copiar el teléfono visible en web.
CA_CONTACT1solo valida la creación nativa de contactos en iOS y Android. Añade el comportamiento web para que QA cubra ambas plataformas.Cambio propuesto
-- CA_CONTACT1: Con teléfono visible en perfil ajeno, en iOS/Android se puede abrir la ficha nativa de nuevo contacto +- CA_CONTACT1: Con teléfono visible en perfil ajeno, en iOS/Android se puede abrir la ficha nativa de nuevo contacto; en web se puede copiar el número visible🤖 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 `@REQUIREMENTS.md` at line 731, Actualiza el requisito CA_CONTACT1 para incluir también el fallback web: cuando el teléfono sea visible en un perfil ajeno, debe poder copiarse desde la web, además de abrir la ficha nativa de nuevo contacto en iOS/Android. Conserva explícitamente ambos comportamientos para que QA valide las tres plataformas.README.md-58-58 (1)
58-58: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAñade el identificador de lenguaje al bloque Markdown.
El bloque que comienza en Line 58 no especifica un lenguaje. Usa
textpara satisfacer MD040.Cambio propuesto
-``` +```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 `@README.md` at line 58, Update the Markdown code block beginning at Line 58 in README.md to include the text language identifier, changing its opening fence to ```text so it satisfies MD040.Source: Linters/SAST tools
.github/workflows/eas.yml-13-13 (1)
13-13: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winConcede
contents: readal jobquality.
permissions: {}limita elGITHUB_TOKENefectivo del workflow reutilizable, aunquequality.ymldeclarecontents: read. Esto puede hacer fallaractions/checkoutygitleaks/gitleaks-action. Manténcontents: writedeshabilitado.🤖 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 @.github/workflows/eas.yml at line 13, Actualiza los permisos del workflow reutilizable para conceder únicamente contents: read al job quality, reemplazando la configuración permissions: {}. Mantén contents: write deshabilitado y no habilites permisos adicionales.src/components/matches/EditMatchTeamModal.tsx-217-224 (1)
217-224: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winEl mensaje de WhatsApp usa el nombre del equipo como título de la partida.
buildInviteShareMessageensrc/lib/shareInvite.ts(líneas 12-21) compone «¡Únete a esta partida en jugaMUS!» seguido deinput.title. AquítitlerecibeteamLabel, que es el nombre del equipo. El receptor lee el nombre del equipo donde espera el título de la partida.Pasa el título de la partida como
titley usa el nombre del equipo enmeta.🐛 Corrección propuesta
Añade el título de la partida a las props de
InviteFriendsTaby deEditMatchTeamModal, y después:const message = buildInviteShareMessage({ kind: 'match', - title: teamLabel, + title: matchTitle, url, - meta: 'Te he invitado a unirte a mi equipo en esta partida', + meta: `Te he invitado a unirte a ${teamLabel} en esta partida`, })🤖 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 `@src/components/matches/EditMatchTeamModal.tsx` around lines 217 - 224, Actualiza InviteFriendsTab y EditMatchTeamModal para recibir y propagar el título de la partida. En el flujo que construye el mensaje con buildInviteShareMessage, pasa el título de la partida en title y mueve teamLabel a meta, conservando el texto contextual existente.src/app/(tabs)/matches/[id].tsx-1162-1167 (1)
1162-1167: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winEl texto del banner es incorrecto y contiene un error gramatical.
Dos problemas en la línea 1166:
- La preposición se duplica. La plantilla ya escribe
unirte a, y la rama negativa añadeal equipo rival. El resultado es «unirte a al equipo rival».- La condición asume que el invitador está en el equipo A. El invitador es el creador de la partida y puede estar en el equipo B. En ese caso el banner dice «su equipo» cuando la invitación es para el equipo rival, y al contrario.
Usa el nombre real del equipo de la invitación en lugar de deducir la relación.
🐛 Corrección propuesta
<Text style={s.inviteBannerText}> - {myPendingInvitation.inviter_name} te ha invitado a participar en la partida y unirte - a {myPendingInvitation.team === TEAM.A ? 'su equipo' : 'al equipo rival'}. + {myPendingInvitation.inviter_name} te ha invitado a participar en la partida y unirte + a {myPendingInvitation.team === TEAM.A ? teamAName : teamBName}. </Text>Nota:
teamANameyteamBNamese calculan en las líneas 861-862, antes de este bloque.🤖 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 `@src/app/`(tabs)/matches/[id].tsx around lines 1162 - 1167, Actualiza el texto del banner en el bloque `myPendingInvitation` para usar el nombre real del equipo invitado, reutilizando `teamAName` o `teamBName` según `myPendingInvitation.team` en lugar de inferirlo con `TEAM.A`. Ajusta la plantilla para que la preposición “a” aparezca una sola vez y elimine el texto fijo “su equipo”/“al equipo rival”.src/types/database.types.ts-2120-2174 (1)
2120-2174: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winLimita las columnas de
get_profile_with_phoney sincroniza su tipo generado.
list_match_invitationsestá correctamente tipada:p.display_nameymi.statussonNOT NULL, por lo questringes correcto.Sin embargo,
get_profile_with_phoneusaSELECT p.*yRETURNS SETOF public.profiles. También devuelvebadge_showcase,notify_on_friend_requestynotify_on_match_invitation, aunquesrc/types/database.types.tsno los declara. Usa una lista explícita de columnas y actualizaReturns/SetofOptionspara evitar exponer preferencias.🤖 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 `@src/types/database.types.ts` around lines 2120 - 2174, Actualiza la función get_profile_with_phone para reemplazar SELECT p.* por una lista explícita de columnas que excluya badge_showcase, notify_on_friend_request y notify_on_match_invitation. Sincroniza en database.types.ts su Returns y SetofOptions con exactamente esas columnas y tipos, manteniendo list_match_invitations sin cambios.src/app/(tabs)/matches/index.tsx-357-376 (1)
357-376: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winNo descartes los errores al responder una invitación.
Si el RPC falla, ambas acciones ocultan el error. La invitación sigue pendiente y el usuario no recibe una causa ni una acción clara para reintentar.
Muestra el mensaje de error de la mutación mediante
Alerto un estado visible.🤖 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 `@src/app/`(tabs)/matches/index.tsx around lines 357 - 376, Update the invitation response handlers in the MatchInvitationRow rendering so rejected respondInvitation.mutateAsync calls no longer use catch(() => undefined). Display the mutation error through Alert or another visible UI state, preserving the existing navigation behavior on success and allowing the user to understand and retry failed responses.
🧹 Nitpick comments (11)
supabase/migrations/20260812100000_108_friendships.sql (1)
283-284: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winRevoque también las escrituras al rol
anon.Supabase concede privilegios por defecto a
anon,authenticatedyservice_role. La línea 283 solo revoca aauthenticated. Hoy la RLS bloquea la escritura porque no existe ninguna políticaINSERT/UPDATE/DELETE, pero la defensa depende de una sola capa.-REVOKE INSERT, UPDATE, DELETE ON public.friendships FROM authenticated; +REVOKE INSERT, UPDATE, DELETE ON public.friendships FROM anon, authenticated;🤖 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 `@supabase/migrations/20260812100000_108_friendships.sql` around lines 283 - 284, Actualiza los privilegios de public.friendships para revocar INSERT, UPDATE y DELETE también al rol anon, manteniendo la concesión de SELECT a authenticated y las revocaciones existentes para authenticated.src/hooks/useFriends.ts (1)
24-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDerive los prefijos de invalidación de los helpers de clave.
Las líneas 35 y 36 repiten los literales
'friend-requests'y'friendship-with-user'. Si cambia un helper de clave, la invalidación deja de coincidir en silencio. Extraiga los prefijos a constantes y úselos en ambos lugares.♻️ Refactor propuesto
+const FRIEND_REQUESTS_KEY = 'friend-requests' as const +const FRIENDSHIP_WITH_USER_KEY = 'friendship-with-user' as const + export function friendRequestsQueryKey(userId: string, direction: 'sent' | 'received') { - return ['friend-requests', userId, direction] as const + return [FRIEND_REQUESTS_KEY, userId, direction] as const } export function friendshipWithUserQueryKey(userId: string, otherUserId: string) { - return ['friendship-with-user', userId, otherUserId] as const + return [FRIENDSHIP_WITH_USER_KEY, userId, otherUserId] as const } function invalidateFriendsQueries(queryClient: ReturnType<typeof useQueryClient>, userId?: string) { if (!userId) return queryClient.invalidateQueries({ queryKey: friendsQueryKey(userId) }) - queryClient.invalidateQueries({ queryKey: ['friend-requests', userId] }) - queryClient.invalidateQueries({ queryKey: ['friendship-with-user', userId] }) + queryClient.invalidateQueries({ queryKey: [FRIEND_REQUESTS_KEY, userId] }) + queryClient.invalidateQueries({ queryKey: [FRIENDSHIP_WITH_USER_KEY, userId] }) }🤖 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 `@src/hooks/useFriends.ts` around lines 24 - 37, Update friendRequestsQueryKey and friendshipWithUserQueryKey to derive their key prefixes from shared constants, then reuse those same constants in invalidateFriendsQueries instead of repeating the 'friend-requests' and 'friendship-with-user' literals. Keep the existing query-key shapes and invalidation behavior unchanged.src/services/friends.service.ts (1)
51-60: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueLa analítica marca envío incluso cuando el RPC acepta la amistad.
send_friend_requestacepta automáticamente si la otra parte ya tenía una solicitud pendiente (migración 108, líneas 80-87). En ese casotrackFriendRequestSentregistrafriend_request_sentaunque el resultado real es una aceptación. Los embudos de PostHog quedan sesgados.Devuelva el estado resultante desde la RPC y elija el evento según ese estado.
🤖 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 `@src/services/friends.service.ts` around lines 51 - 60, Actualiza sendFriendRequest y la RPC send_friend_request para devolver también el estado resultante de la relación, distinguiendo una solicitud enviada de una amistad aceptada automáticamente. Usa ese estado para registrar el evento analítico correspondiente en lugar de llamar siempre a trackFriendRequestSent; conserva la validación de errores y el retorno del identificador.src/services/friends.service.test.ts (1)
102-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAñada casos para los caminos de datos nulos.
Las pruebas no cubren dos ramas del servicio:
sendFriendRequestcondata: nullyerror: null, que debe lanzar'No se pudo enviar la solicitud'; ylistMyFriends/listMyFriendRequestscondata: null, que deben devolver[].💚 Pruebas propuestas
+ it('sendFriendRequest lanza cuando la rpc no devuelve id', async () => { + mockRpc({ data: null, error: null }) + await expect(sendFriendRequest('user-6')).rejects.toThrow('No se pudo enviar la solicitud') + }) + + it('listMyFriends devuelve un array vacío cuando data es null', async () => { + mockRpc({ data: null, error: null }) + await expect(listMyFriends()).resolves.toEqual([]) + }) + it('listMyFriendRequests forwards direction', async () => {🤖 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 `@src/services/friends.service.test.ts` around lines 102 - 120, Amplía las pruebas de friends.service para cubrir respuestas RPC con data: null y error: null: añade un caso para sendFriendRequest que espere el rechazo con 'No se pudo enviar la solicitud', y casos para listMyFriends y listMyFriendRequests que verifiquen que devuelven []. Reutiliza mockRpc y conserva las expectativas existentes.src/components/profile/SendFriendRequestModal.tsx (1)
71-80: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAlinee el límite de longitud del mensaje con la base de datos.
El cliente limita el mensaje a 200 caracteres, pero
public.friendships.messageesTEXTsin restricción (migración 108, línea 13). Cualquier otro cliente puede almacenar textos arbitrarios que después se muestran enFriendsSection. Añada unCHECK (char_length(message) <= 200)o recorte el valor dentro desend_friend_request.🤖 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 `@src/components/profile/SendFriendRequestModal.tsx` around lines 71 - 80, Align the database constraint with the 200-character limit enforced by the message Input: update the friendships schema or send_friend_request path so message values are always limited to 200 characters, preferably adding CHECK (char_length(message) <= 200) or truncating within send_friend_request. Ensure requests from non-UI clients cannot store longer messages displayed by FriendsSection.src/utils/leagueFixtures.test.ts (1)
4-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAñade invariantes de emparejamiento y segunda vuelta.
Estas pruebas solo verifican conteos. Un calendario que duplique un emparejamiento y omita otro puede pasar ambas pruebas. Verifica que cada emparejamiento no ordenado ocurra una vez y que cada fixture de segunda vuelta invierta el fixture correspondiente.
Propuesta de pruebas
it('generates correct count for 4 pairs single round', () => { const ids = ['a', 'b', 'c', 'd'] const fixtures = generateRoundRobinFixtures(ids, false) expect(fixtures).toHaveLength(expectedMatchCount(4, false)) expect(fixtures.every((f) => !f.isSecondLeg)).toBe(true) + expect(fixtures.every((f) => f.pairAId !== f.pairBId)).toBe(true) + expect( + new Set( + fixtures.map((f) => [f.pairAId, f.pairBId].sort().join(':')) + ) + ).toHaveLength(expectedMatchCount(4, false)) }) it('doubles fixtures for double round', () => { const ids = ['a', 'b', 'c', 'd'] const fixtures = generateRoundRobinFixtures(ids, true) + const firstLeg = fixtures.filter((f) => !f.isSecondLeg) + const secondLeg = fixtures.filter((f) => f.isSecondLeg) expect(fixtures).toHaveLength(expectedMatchCount(4, true)) - expect(fixtures.filter((f) => f.isSecondLeg)).toHaveLength(expectedMatchCount(4, false)) + expect(secondLeg).toHaveLength(expectedMatchCount(4, false)) + for (const fixture of firstLeg) { + expect(secondLeg).toContainEqual({ + pairAId: fixture.pairBId, + pairBId: fixture.pairAId, + roundNumber: fixture.roundNumber + ids.length - 1, + isSecondLeg: true, + }) + } })🤖 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 `@src/utils/leagueFixtures.test.ts` around lines 4 - 16, Amplía las pruebas alrededor de generateRoundRobinFixtures para validar invariantes, no solo cantidades: comprueba que cada par no ordenado de ids aparece exactamente una vez en la primera vuelta y que no existen emparejamientos duplicados u omitidos. Para el modo de doble vuelta, verifica que cada fixture marcado como isSecondLeg invierte los participantes del fixture correspondiente de la primera vuelta.supabase/migrations/20260812100002_110_remove_friend_and_invite_notifications.sql (1)
135-140: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
remove_frienddeja invitaciones a partidas pendientes entre los antiguos amigos.
invite_friend_to_matchexige una amistad aceptada (líneas 187-194 desupabase/migrations/20260812100001_109_match_invitations.sql).respond_match_invitationno vuelve a comprobar la amistad. Después de eliminar la amistad, el otro usuario puede seguir aceptando una invitación pendiente y unirse a la partida.Cancela las invitaciones pendientes entre ambos usuarios dentro de la misma transacción.
♻️ Refactor propuesto
IF NOT FOUND THEN RAISE EXCEPTION 'friendship_not_found'; END IF; + + -- Una invitación pendiente ya no es válida sin amistad. + UPDATE public.match_invitations + SET status = 'cancelled', responded_at = NOW() + WHERE status = 'pending' + AND ( + (inviter_id = v_self AND invitee_id = p_other_user_id) + OR (inviter_id = p_other_user_id AND invitee_id = v_self) + ); END;🤖 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 `@supabase/migrations/20260812100002_110_remove_friend_and_invite_notifications.sql` around lines 135 - 140, Update remove_friend, immediately after deleting the accepted friendship and before returning, to cancel all pending match invitations between v_self and p_other_user_id within the same transaction. Reuse the invitation table’s pending-status representation and match either user as inviter or invitee, while preserving the existing friendship_not_found behavior.src/utils/notificationPrefs.ts (1)
73-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDeriva la copia de preferencias de
EVENT_FIELDSpara eliminar la duplicación.Ambas funciones copian los campos de evento uno a uno. Añadir una preferencia obliga a modificar seis lugares: el tipo,
EVENT_FIELDS,ALL_ON,ALL_OFFy las dos funciones. Este cambio lo demuestra:notify_on_friend_requestynotify_on_match_invitationse repiten en las líneas 82-83 y 103-104. Si se omite una copia, la preferencia se pierde de forma silenciosa en cada guardado.Además, el
satisfiesde la línea 28 comprueba que los miembros deEVENT_FIELDSsean válidos, pero no que la lista sea exhaustiva.♻️ Refactor propuesto
+type EventField = Exclude<NotificationPrefField, 'notify_push'> + +function pickEvents(profile: NotificationPrefState): Omit<NotificationPrefState, 'notify_push'> { + return Object.fromEntries( + EVENT_FIELDS.map((key) => [key, profile[key]]) + ) as Omit<NotificationPrefState, 'notify_push'> +} + export function buildNotifUpdates( profile: NotificationPrefState, field: NotificationPrefField, value: boolean ): NotificationPrefState { if (field === 'notify_push') { return value ? { ...ALL_ON } : { ...ALL_OFF } } - return withMasterPush({ - notify_on_join: profile.notify_on_join, - ... - notify_on_match_invitation: profile.notify_on_match_invitation, - [field]: value, - }) + return withMasterPush({ ...pickEvents(profile), [field]: value }) } export function buildReminderTimingUpdates( profile: NotificationPrefState, timing: '24h' | '2h', enabled: boolean ): NotificationPrefState { - return withMasterPush({ - notify_on_join: profile.notify_on_join, - ... - notify_on_match_invitation: profile.notify_on_match_invitation, - }) + return withMasterPush({ + ...pickEvents(profile), + notify_on_reminder_24h: timing === '24h' ? enabled : profile.notify_on_reminder_24h, + notify_on_reminder_2h: timing === '2h' ? enabled : profile.notify_on_reminder_2h, + }) }Para asegurar la exhaustividad de
EVENT_FIELDS, añade una comprobación de tipos:// Falla la compilación si falta un campo de evento en EVENT_FIELDS. type _EventFieldsExhaustive = Exclude<EventField, (typeof EVENT_FIELDS)[number]> extends never ? true : never const _eventFieldsExhaustive: _EventFieldsExhaustive = trueAlso applies to: 94-105
🤖 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 `@src/utils/notificationPrefs.ts` around lines 73 - 85, Deriva las copias de preferencias de las funciones afectadas a partir de EVENT_FIELDS, eliminando las asignaciones manuales repetidas y conservando la sobrescritura de [field] cuando corresponda. Añade junto a la definición de EVENT_FIELDS una comprobación de tipos que garantice que incluya todos los miembros de EventField, manteniendo también la validación satisfies existente.src/utils/notificationPrefs.test.ts (1)
98-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winEste test duplica el de las líneas 66-71 y no verifica lo que su nombre indica.
La llamada y la expectativa son idénticas a las del test «toggles 24h independently and keeps 2h». Con el fixture
allOn, todas las preferencias valentrue, por lo que la aserción pasa incluso sibuildReminderTimingUpdatesrecalculase las nuevas preferencias en vez de preservarlas.Usa un estado donde las nuevas preferencias difieran del resto para que la aserción distinga la preservación.
💚 Refactor propuesto
it('reminder timing updates preserve the new preferences', () => { - expect(buildReminderTimingUpdates(allOn, '24h', false)).toEqual({ - ...allOn, - notify_on_reminder_24h: false, - }) + const mixed = { + ...allOff, + notify_push: true, + notify_on_reminder_24h: true, + notify_on_friend_request: true, + notify_on_match_invitation: false, + } + expect(buildReminderTimingUpdates(mixed, '2h', true)).toEqual({ + ...mixed, + notify_on_reminder_2h: true, + notify_on_friend_request: true, + notify_on_match_invitation: false, + }) })🤖 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 `@src/utils/notificationPrefs.test.ts` around lines 98 - 102, Actualiza el test “reminder timing updates preserve the new preferences” para usar un fixture donde los valores de las nuevas preferencias difieran del resto, en lugar de reutilizar allOn. Mantén la llamada a buildReminderTimingUpdates y verifica que notify_on_reminder_24h cambie según el argumento mientras las demás preferencias nuevas conserven sus valores previos, diferenciándolo del test “toggles 24h independently and keeps 2h”.supabase/migrations/20260812100001_109_match_invitations.sql (1)
720-720: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
ALTER PUBLICATION ... ADD TABLEno es idempotente.La migración usa
CREATE TABLE IF NOT EXISTSyCREATE INDEX IF NOT EXISTS, por lo que se puede volver a ejecutar.ALTER PUBLICATION ... ADD TABLElanza un error si la tabla ya pertenece a la publicación. Una segunda ejecución aborta la migración.♻️ Refactor propuesto
-ALTER PUBLICATION supabase_realtime ADD TABLE public.match_invitations; +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_publication_tables + WHERE pubname = 'supabase_realtime' + AND schemaname = 'public' + AND tablename = 'match_invitations' + ) THEN + ALTER PUBLICATION supabase_realtime ADD TABLE public.match_invitations; + END IF; +END $$;🤖 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 `@supabase/migrations/20260812100001_109_match_invitations.sql` at line 720, Haz idempotente la configuración de realtime para match_invitations: reemplaza el ALTER PUBLICATION directo por una comprobación de pertenencia a la publicación y ejecuta ADD TABLE solo cuando la tabla aún no esté incluida. Conserva la publicación y la tabla existentes sin provocar errores al reejecutar la migración.src/components/matches/EditMatchTeamModal.tsx (1)
211-211: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winUsa
isLoadingpara mostrar el estado de carga de amigos.
useMyFriendsusa React Query v5 conenabled: Boolean(userId). Sin sesión,isPendingpermanece activo aunque la consulta no se esté ejecutando, y la pestaña muestra «Cargando amigos…» indefinidamente.🤖 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 `@src/components/matches/EditMatchTeamModal.tsx` at line 211, En EditMatchTeamModal, cambia la desestructuración de useMyFriends para usar isLoading en lugar de isPending al controlar el estado de carga de amigos. Mantén el resto del flujo y la visualización de amigos sin cambios.
🤖 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 `@src/app/`(tabs)/matches/[id].tsx:
- Around line 1474-1476: Restringe el valor de matchId pasado a
EditMatchTeamModal para que solo esté definido cuando la partida sea
independiente (sin tournament_id ni league_id), el usuario actual sea su creador
y el estado sea planned o in_progress; en cualquier otro caso pásalo como
undefined. Mantén team y freeSlots sin cambios para que la pestaña «Añadir
amigo» solo aparezca cuando la RPC invite_friend_to_match puede aceptar la
invitación.
In `@src/app/`(tabs)/matches/create.tsx:
- Around line 209-232: Actualiza toggleInvite y el flujo de creación de partidas
para mantener las invitaciones dentro de los huecos disponibles. Recorta
invitesA e invitesB cuando cambien freeSlotsA o freeSlotsB, eliminando
selecciones excedentes; antes de crear una partida pasada, valida que cada
equipo tenga exactamente tantas invitaciones como huecos sin nombre y evita
crear pending_validation si falta alguna.
- Around line 353-393: Add a local submission state covering the entire onSubmit
flow after createMatch, including result submission, friend invitations, and
WhatsApp sharing. Set it before starting the flow, clear it in a finally block,
and bind the create/submit button’s loading state to this flag so additional
taps remain blocked until all work completes.
- Around line 373-392: Actualiza el flujo de invitaciones dentro de Promise.all
para acumular por separado los fallos de inviteFriend.mutateAsync y de
shareInviteViaWhatsApp. Mantén la creación de la partida no bloqueante, pero
muestra al creador un aviso con los invitados fallidos y una acción para
reintentar sus envíos; registra el fallo de WhatsApp únicamente después de
confirmar que el RPC creó la invitación.
In `@src/app/`(tabs)/profile/[userId].tsx:
- Line 223: Elimina la unión local FriendshipStatus y reutiliza el tipo
FriendshipStatusRow exportado por useFriends. Añade la importación de tipo
indicada y actualiza las referencias de FriendshipStatus para usar el alias
importado, manteniendo el comportamiento existente.
In `@src/components/matches/EditMatchTeamModal.tsx`:
- Around line 230-232: Actualiza el componente EditMatchTeamModal para mantener
en estado local los amigos invitados correctamente, añadiéndolos tras el éxito
de la mutación. Usa ese estado junto con freeSlots para impedir nuevas
invitaciones cuando se alcance la capacidad y aplica invite.isPending únicamente
a la fila cuyo amigo está siendo invitado, en lugar de a todos los botones.
In `@src/services/matchInvitations.service.ts`:
- Around line 80-84: Update respondMatchInvitation in
src/services/matchInvitations.service.ts:80-84 so the accepted-invitation
tracking uses the real match_id and team returned by respond_match_invitation,
or values supplied by its caller, instead of invitationId and an empty team.
Update the mock and assertion in
src/services/matchInvitations.service.test.ts:67-78 to verify the actual
match_id and team metadata.
In `@src/services/profiles.service.ts`:
- Line 82: En la rama de getProfile para un userId ajeno, elimina todas las
columnas notify_* de la proyección y ajusta el tipo o la respuesta para devolver
valores neutros sin esas preferencias. Conserva las columnas de notificación
únicamente en get_own_profile.
In
`@supabase/migrations/20260811032000_104_player_stats_async_recompute_queue.sql`:
- Around line 104-111: Actualiza el bloque de desprogramación alrededor de
cron.unschedule para capturar únicamente la condición específica de trabajo
inexistente, eliminando el manejo WHEN OTHERS. Propaga cualquier error de
permisos, configuración o catálogo, manteniendo el comportamiento tolerante solo
cuando process-player-stats-recompute-queue no exista.
In `@supabase/migrations/20260812100000_108_friendships.sql`:
- Around line 90-98: Limita la reapertura de solicitudes previamente rechazadas
mediante un período de espera basado en la fecha de respuesta, evitando
actualizar la fila cuando el destinatario la rechazó recientemente y devolviendo
el error `request_recently_rejected`. Ajusta el flujo de reapertura de
`friendships` para conservar el comportamiento actual tras `cancelled` y después
de que expire el límite, y añade el mapeo de `request_recently_rejected` en
`mapFriendRpcError`.
In `@supabase/migrations/20260812100001_109_match_invitations.sql`:
- Around line 314-327: Revalida la capacidad del equipo en la aceptación de
invitaciones, dentro del flujo que actualiza o inserta en match_participants y
con la fila de matches ya bloqueada, reutilizando
inviter_team_capacity_available antes de cualquier cambio y rechazando la
operación si no hay plaza. En
src/components/matches/EditMatchTeamModal.tsx:230-232, descuenta de freeSlots
las invitaciones enviadas durante la sesión y deshabilita el botón cuando el
resultado sea cero; ambos sitios requieren cambios.
- Around line 266-272: Actualiza el SELECT que carga v_inv en la función de
procesamiento de invitaciones para incluir FOR UPDATE y bloquear la fila antes
de validar invitee_id y status. Mantén las comprobaciones existentes y el
bloqueo posterior de v_match sin cambios.
- Around line 412-437: Update list_match_invitations to authorize the caller for
p_match_id before returning invitations, matching the access-check pattern used
by the other read RPCs. Ensure authenticated users can only retrieve invitee_id
and invitee_name data for matches they are permitted to access, while
unauthorized callers receive no rows or an authorization error.
- Around line 480-481: Actualiza la función match_effective_roster_filled(UUID)
para ejecutarse como SECURITY DEFINER y revoca su privilegio EXECUTE para
PUBLIC. Mantén el acceso interno a match_pending_invitations_filled(UUID)
mediante el contexto del definidor, sin conceder acceso público directo a esa
función auxiliar.
In
`@supabase/migrations/20260812100002_110_remove_friend_and_invite_notifications.sql`:
- Around line 9-25: Update the profile privileges in the migration around the
GRANT SELECT and GRANT UPDATE statements to include the existing badge_showcase
column in both column lists. Preserve the other granted columns, including
notify_on_match_change and notify_on_reminder.
---
Other comments:
In @.github/workflows/eas.yml:
- Line 13: Actualiza los permisos del workflow reutilizable para conceder
únicamente contents: read al job quality, reemplazando la configuración
permissions: {}. Mantén contents: write deshabilitado y no habilites permisos
adicionales.
In `@README.md`:
- Line 58: Update the Markdown code block beginning at Line 58 in README.md to
include the text language identifier, changing its opening fence to ```text so
it satisfies MD040.
In `@REQUIREMENTS.md`:
- Around line 173-174: Define one consistent visibility contract for Descubrir
across REQUIREMENTS.md lines 173-174 and 249-250, and README.md lines 188-189:
explicitly state whether private password-protected entities appear in discovery
and document the password requirement consistently in all three locations,
including the private-league listing.
- Line 731: Actualiza el requisito CA_CONTACT1 para incluir también el fallback
web: cuando el teléfono sea visible en un perfil ajeno, debe poder copiarse
desde la web, además de abrir la ficha nativa de nuevo contacto en iOS/Android.
Conserva explícitamente ambos comportamientos para que QA valide las tres
plataformas.
In `@src/app/`(tabs)/matches/[id].tsx:
- Around line 1162-1167: Actualiza el texto del banner en el bloque
`myPendingInvitation` para usar el nombre real del equipo invitado, reutilizando
`teamAName` o `teamBName` según `myPendingInvitation.team` en lugar de inferirlo
con `TEAM.A`. Ajusta la plantilla para que la preposición “a” aparezca una sola
vez y elimine el texto fijo “su equipo”/“al equipo rival”.
In `@src/app/`(tabs)/matches/index.tsx:
- Around line 357-376: Update the invitation response handlers in the
MatchInvitationRow rendering so rejected respondInvitation.mutateAsync calls no
longer use catch(() => undefined). Display the mutation error through Alert or
another visible UI state, preserving the existing navigation behavior on success
and allowing the user to understand and retry failed responses.
In `@src/app/`(tabs)/profile/[userId].tsx:
- Around line 251-257: Update the accepted-friend indicator View in the status
=== 'accepted' branch to be an accessible element: enable accessibility and
assign the appropriate accessibilityRole alongside the existing
accessibilityLabel, so screen readers reliably announce the accepted friendship
state.
In `@src/components/matches/EditMatchTeamModal.tsx`:
- Around line 217-224: Actualiza InviteFriendsTab y EditMatchTeamModal para
recibir y propagar el título de la partida. En el flujo que construye el mensaje
con buildInviteShareMessage, pasa el título de la partida en title y mueve
teamLabel a meta, conservando el texto contextual existente.
In `@src/components/profile/FriendsSection.tsx`:
- Around line 201-240: Update FriendsSection’s request action state to track
in-flight friendship_id values per mutation, rather than relying on shared
respond.isPending or cancel.isPending. Use the tracked IDs to set loading only
on the matching row’s accept/reject or cancel Button, and remove each ID when
its mutation settles; do not derive this from respond.variables or
cancel.variables.
In `@src/types/database.types.ts`:
- Around line 2120-2174: Actualiza la función get_profile_with_phone para
reemplazar SELECT p.* por una lista explícita de columnas que excluya
badge_showcase, notify_on_friend_request y notify_on_match_invitation.
Sincroniza en database.types.ts su Returns y SetofOptions con exactamente esas
columnas y tipos, manteniendo list_match_invitations sin cambios.
In `@supabase/migrations/20260812100000_108_friendships.sql`:
- Around line 80-88: En el bloque de estado pendiente de la función que
actualiza public.friendships, encole una notificación de aceptación mediante
public.enqueue_notification antes de retornar v_row.id cuando se realiza la
aceptación automática. Use los identificadores y destinatarios del registro
existente para notificar al solicitante original, manteniendo intactos la
actualización y el flujo de error restante.
- Line 288: Make the ALTER PUBLICATION statement for public.friendships
idempotent by using the supported conditional form, so rerunning the migration
succeeds when the table is already part of supabase_realtime.
---
Nitpick comments:
In `@src/components/matches/EditMatchTeamModal.tsx`:
- Line 211: En EditMatchTeamModal, cambia la desestructuración de useMyFriends
para usar isLoading en lugar de isPending al controlar el estado de carga de
amigos. Mantén el resto del flujo y la visualización de amigos sin cambios.
In `@src/components/profile/SendFriendRequestModal.tsx`:
- Around line 71-80: Align the database constraint with the 200-character limit
enforced by the message Input: update the friendships schema or
send_friend_request path so message values are always limited to 200 characters,
preferably adding CHECK (char_length(message) <= 200) or truncating within
send_friend_request. Ensure requests from non-UI clients cannot store longer
messages displayed by FriendsSection.
In `@src/hooks/useFriends.ts`:
- Around line 24-37: Update friendRequestsQueryKey and
friendshipWithUserQueryKey to derive their key prefixes from shared constants,
then reuse those same constants in invalidateFriendsQueries instead of repeating
the 'friend-requests' and 'friendship-with-user' literals. Keep the existing
query-key shapes and invalidation behavior unchanged.
In `@src/services/friends.service.test.ts`:
- Around line 102-120: Amplía las pruebas de friends.service para cubrir
respuestas RPC con data: null y error: null: añade un caso para
sendFriendRequest que espere el rechazo con 'No se pudo enviar la solicitud', y
casos para listMyFriends y listMyFriendRequests que verifiquen que devuelven [].
Reutiliza mockRpc y conserva las expectativas existentes.
In `@src/services/friends.service.ts`:
- Around line 51-60: Actualiza sendFriendRequest y la RPC send_friend_request
para devolver también el estado resultante de la relación, distinguiendo una
solicitud enviada de una amistad aceptada automáticamente. Usa ese estado para
registrar el evento analítico correspondiente en lugar de llamar siempre a
trackFriendRequestSent; conserva la validación de errores y el retorno del
identificador.
In `@src/utils/leagueFixtures.test.ts`:
- Around line 4-16: Amplía las pruebas alrededor de generateRoundRobinFixtures
para validar invariantes, no solo cantidades: comprueba que cada par no ordenado
de ids aparece exactamente una vez en la primera vuelta y que no existen
emparejamientos duplicados u omitidos. Para el modo de doble vuelta, verifica
que cada fixture marcado como isSecondLeg invierte los participantes del fixture
correspondiente de la primera vuelta.
In `@src/utils/notificationPrefs.test.ts`:
- Around line 98-102: Actualiza el test “reminder timing updates preserve the
new preferences” para usar un fixture donde los valores de las nuevas
preferencias difieran del resto, en lugar de reutilizar allOn. Mantén la llamada
a buildReminderTimingUpdates y verifica que notify_on_reminder_24h cambie según
el argumento mientras las demás preferencias nuevas conserven sus valores
previos, diferenciándolo del test “toggles 24h independently and keeps 2h”.
In `@src/utils/notificationPrefs.ts`:
- Around line 73-85: Deriva las copias de preferencias de las funciones
afectadas a partir de EVENT_FIELDS, eliminando las asignaciones manuales
repetidas y conservando la sobrescritura de [field] cuando corresponda. Añade
junto a la definición de EVENT_FIELDS una comprobación de tipos que garantice
que incluya todos los miembros de EventField, manteniendo también la validación
satisfies existente.
In `@supabase/migrations/20260812100000_108_friendships.sql`:
- Around line 283-284: Actualiza los privilegios de public.friendships para
revocar INSERT, UPDATE y DELETE también al rol anon, manteniendo la concesión de
SELECT a authenticated y las revocaciones existentes para authenticated.
In `@supabase/migrations/20260812100001_109_match_invitations.sql`:
- Line 720: Haz idempotente la configuración de realtime para match_invitations:
reemplaza el ALTER PUBLICATION directo por una comprobación de pertenencia a la
publicación y ejecuta ADD TABLE solo cuando la tabla aún no esté incluida.
Conserva la publicación y la tabla existentes sin provocar errores al reejecutar
la migración.
In
`@supabase/migrations/20260812100002_110_remove_friend_and_invite_notifications.sql`:
- Around line 135-140: Update remove_friend, immediately after deleting the
accepted friendship and before returning, to cancel all pending match
invitations between v_self and p_other_user_id within the same transaction.
Reuse the invitation table’s pending-status representation and match either user
as inviter or invitee, while preserving the existing friendship_not_found
behavior.
🪄 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: Organization UI
Review profile: QUIET
Plan: Pro Plus
Run ID: 838a9463-c7fb-4f3e-bea3-57c784d0393e
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (34)
.github/workflows/eas.ymlREADME.mdREQUIREMENTS.mdTASKS.mdapp.jsonpackage.jsonsrc/app/(tabs)/matches/[id].tsxsrc/app/(tabs)/matches/create.tsxsrc/app/(tabs)/matches/index.tsxsrc/app/(tabs)/profile/[userId].tsxsrc/app/(tabs)/profile/index.tsxsrc/components/matches/EditMatchTeamModal.tsxsrc/components/profile/FriendsSection.tsxsrc/components/profile/SendFriendRequestModal.tsxsrc/hooks/useFriends.tssrc/hooks/useMatchInvitations.tssrc/lib/analytics.test.tssrc/lib/analytics.tssrc/services/friends.service.test.tssrc/services/friends.service.tssrc/services/matchInvitations.service.test.tssrc/services/matchInvitations.service.tssrc/services/profiles.service.tssrc/types/database.types.tssrc/utils/leagueFixtures.test.tssrc/utils/leagueFixtures.tssrc/utils/notificationPrefs.test.tssrc/utils/notificationPrefs.tssupabase/functions/process-notifications/index.tssupabase/migrations/20260811032000_104_player_stats_async_recompute_queue.sqlsupabase/migrations/20260811150000_106_security_hardening.sqlsupabase/migrations/20260812100000_108_friendships.sqlsupabase/migrations/20260812100001_109_match_invitations.sqlsupabase/migrations/20260812100002_110_remove_friend_and_invite_notifications.sql
| await Promise.all( | ||
| [ | ||
| ...invitesA.map((fid) => ({ fid, team: TEAM.A })), | ||
| ...invitesB.map((fid) => ({ fid, team: TEAM.B })), | ||
| ].map(async ({ fid, team }) => { | ||
| try { | ||
| await inviteFriend.mutateAsync({ matchId: match.id, inviteeId: fid, team }) | ||
| const message = buildInviteShareMessage({ | ||
| kind: 'match', | ||
| title: values.title?.trim() || DEFAULT_MATCH_TITLE, | ||
| url: shareUrl, | ||
| meta: 'Te he invitado a participar en esta partida', | ||
| }) | ||
| await shareInviteViaWhatsApp(message) | ||
| } catch (err) { | ||
| // Best-effort: a failed invite must not abort match creation. | ||
|
|
||
| console.warn('invite_friend_failed', err) | ||
| } | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Muestra los fallos de invitación al creador.
El catch oculta todos los fallos y redirige al usuario. La partida queda creada sin algunos invitados y el usuario no puede identificar ni reintentar los envíos fallidos.
Mantén la creación no bloqueante, pero acumula los fallos de RPC y muestra un aviso con una acción de reintento. Registra por separado un fallo de WhatsApp después de que el RPC haya creado la invitación.
🤖 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 `@src/app/`(tabs)/matches/create.tsx around lines 373 - 392, Actualiza el flujo
de invitaciones dentro de Promise.all para acumular por separado los fallos de
inviteFriend.mutateAsync y de shareInviteViaWhatsApp. Mantén la creación de la
partida no bloqueante, pero muestra al creador un aviso con los invitados
fallidos y una acción para reintentar sus envíos; registra el fallo de WhatsApp
únicamente después de confirmar que el RPC creó la invitación.
| SELECT * INTO v_inv FROM public.match_invitations WHERE id = p_invitation_id; | ||
| IF NOT FOUND THEN RAISE EXCEPTION 'invitation_not_found'; END IF; | ||
| IF v_inv.invitee_id <> auth.uid() THEN RAISE EXCEPTION 'not_invitee'; END IF; | ||
| IF v_inv.status <> 'pending' THEN RAISE EXCEPTION 'not_pending'; END IF; | ||
|
|
||
| SELECT * INTO v_match FROM public.matches WHERE id = v_inv.match_id FOR UPDATE; | ||
| IF NOT FOUND THEN RAISE EXCEPTION 'match_not_found'; END IF; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Bloquea la fila de la invitación para evitar respuestas concurrentes.
El SELECT de la línea 266 no usa FOR UPDATE. Dos llamadas concurrentes con el mismo p_invitation_id leen el estado pending a la vez. Ambas superan la comprobación de la línea 269. En el rechazo, ambas cancelan la partida, insertan dos filas en match_state_transitions y encolan notificaciones duplicadas para los participantes. El bloqueo de matches de la línea 271 se adquiere después de la comprobación, por lo que no elimina la condición de carrera.
🔒 Corrección propuesta
- SELECT * INTO v_inv FROM public.match_invitations WHERE id = p_invitation_id;
+ SELECT * INTO v_inv FROM public.match_invitations
+ WHERE id = p_invitation_id FOR UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'invitation_not_found'; END IF;📝 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.
| SELECT * INTO v_inv FROM public.match_invitations WHERE id = p_invitation_id; | |
| IF NOT FOUND THEN RAISE EXCEPTION 'invitation_not_found'; END IF; | |
| IF v_inv.invitee_id <> auth.uid() THEN RAISE EXCEPTION 'not_invitee'; END IF; | |
| IF v_inv.status <> 'pending' THEN RAISE EXCEPTION 'not_pending'; END IF; | |
| SELECT * INTO v_match FROM public.matches WHERE id = v_inv.match_id FOR UPDATE; | |
| IF NOT FOUND THEN RAISE EXCEPTION 'match_not_found'; END IF; | |
| SELECT * INTO v_inv FROM public.match_invitations | |
| WHERE id = p_invitation_id FOR UPDATE; | |
| IF NOT FOUND THEN RAISE EXCEPTION 'invitation_not_found'; END IF; | |
| IF v_inv.invitee_id <> auth.uid() THEN RAISE EXCEPTION 'not_invitee'; END IF; | |
| IF v_inv.status <> 'pending' THEN RAISE EXCEPTION 'not_pending'; END IF; | |
| SELECT * INTO v_match FROM public.matches WHERE id = v_inv.match_id FOR UPDATE; | |
| IF NOT FOUND THEN RAISE EXCEPTION 'match_not_found'; END IF; |
🤖 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 `@supabase/migrations/20260812100001_109_match_invitations.sql` around lines
266 - 272, Actualiza el SELECT que carga v_inv en la función de procesamiento de
invitaciones para incluir FOR UPDATE y bloquear la fila antes de validar
invitee_id y status. Mantén las comprobaciones existentes y el bloqueo posterior
de v_match sin cambios.
| REVOKE ALL ON FUNCTION public.match_pending_invitations_filled(UUID) FROM PUBLIC; | ||
| REVOKE ALL ON FUNCTION public.inviter_team_capacity_available(UUID, TEXT) FROM PUBLIC; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Localizar llamadores de las funciones auxiliares y comprobar si son SECURITY DEFINER.
rg -n "match_effective_roster_filled|match_pending_invitations_filled" --glob '*.sql' supabase
echo "--- definiciones con contexto de seguridad ---"
fd -e sql . supabase/migrations --exec rg -n -B 2 -A 12 "match_effective_roster_filled" {}Repository: Japegomez/jugaMUS
Length of output: 17515
🏁 Script executed:
#!/bin/bash
set -eu
file="supabase/migrations/20260812100001_109_match_invitations.sql"
echo "--- migration definitions and grants ---"
sed -n '45,105p;460,490p' "$file"
echo "--- function definitions for direct callers ---"
rg -n -B 8 -A 35 \
"CREATE( OR REPLACE)? FUNCTION public\.(match_effective_roster_filled|match_pending_invitations_filled|inviter_team_capacity_available)" \
supabase/migrations
echo "--- security-definer declarations in those definitions ---"
python3 - <<'PY'
from pathlib import Path
import re
targets = {
"match_effective_roster_filled",
"match_pending_invitations_filled",
"inviter_team_capacity_available",
}
for path in Path("supabase/migrations").glob("*.sql"):
text = path.read_text(errors="replace")
for name in targets:
for m in re.finditer(
rf"CREATE(?: OR REPLACE)? FUNCTION\s+public\.{name}\s*\([^)]*\)",
text, re.I,
):
body = text[m.end(): text.find("$$;", m.end()) if "$$;" in text[m.end():] else m.end()+1000]
header = text[m.start():m.end()]
print(f"{path}:{text.count(chr(10), 0, m.start())+1}: {name}")
print(" SECURITY DEFINER:", bool(re.search(r"\bSECURITY\s+DEFINER\b", header + body, re.I)))
print(" header:", " ".join(header.split()))
PYRepository: Japegomez/jugaMUS
Length of output: 17822
🏁 Script executed:
#!/bin/bash
set -eu
echo "--- grants and revokes for roster functions ---"
rg -n -i "GRANT|REVOKE|match_effective_roster_filled|match_pending_invitations_filled" \
supabase/migrations \
| rg -i "GRANT|REVOKE|match_effective_roster_filled|match_pending_invitations_filled"
echo "--- containing function definitions and security mode ---"
python3 - <<'PY'
from pathlib import Path
import re
targets = ("match_effective_roster_filled", "match_pending_invitations_filled")
pat = re.compile(
r"CREATE(?: OR REPLACE)? FUNCTION\s+"
r"(?:[A-Za-z_][\w]*\.)?[A-Za-z_][\w]*\s*\([^;]*?\)"
r".*?AS\s+\$\$(.*?)\$\$;",
re.I | re.S,
)
for path in sorted(Path("supabase/migrations").glob("*.sql")):
text = path.read_text(errors="replace")
for m in pat.finditer(text):
body = m.group(1)
if not any(re.search(rf"\b{re.escape(t)}\s*\(", body, re.I) for t in targets):
continue
start = text.count("\n", 0, m.start()) + 1
header = text[m.start():m.start() + text[m.start():].find("AS $$")]
name = re.search(r"FUNCTION\s+((?:\w+\.)?\w+)\s*\(", header, re.I).group(1)
security = "SECURITY DEFINER" if re.search(r"\bSECURITY\s+DEFINER\b", header, re.I) else "SECURITY INVOKER/default"
print(f"{path}:{start}: {name}: {security}")
for i, line in enumerate(body.splitlines(), start=start):
if any(t in line for t in targets):
print(f" {i}: {line.strip()}")
echo "--- non-function call sites (policies, views, triggers, statements) ---"
python3 - <<'PY'
from pathlib import Path
import re
targets = ("match_effective_roster_filled", "match_pending_invitations_filled")
for path in sorted(Path("supabase/migrations").glob("*.sql")):
lines = path.read_text(errors="replace").splitlines()
in_function = False
dollar = None
for n, line in enumerate(lines, 1):
if re.search(r"\bCREATE(?: OR REPLACE)? FUNCTION\b", line, re.I):
in_function = True
if in_function and dollar is None:
m = re.search(r"\bAS\s+(\$[^$]*\$)", line, re.I)
if m:
dollar = m.group(1)
if in_function and any(re.search(rf"\b{re.escape(t)}\s*\(", line, re.I) for t in targets):
continue
if not in_function and any(re.search(rf"\b{re.escape(t)}\s*\(", line, re.I) for t in targets):
print(f"{path}:{n}:{line.strip()}")
if in_function and dollar and dollar in line and line.strip() != dollar:
in_function = False
dollar = None
PYRepository: Japegomez/jugaMUS
Length of output: 50373
🏁 Script executed:
#!/bin/bash
python3 - <<'PY'
from pathlib import Path
import re
targets = ("match_effective_roster_filled", "match_pending_invitations_filled")
def dollar_at(line):
m = re.search(r"\bAS\s+(\$[A-Za-z_0-9]*\$)", line, re.I)
return m.group(1) if m else None
for path in sorted(Path("supabase/migrations").glob("*.sql")):
lines = path.read_text(errors="replace").splitlines()
i = 0
while i < len(lines):
if not re.search(r"\bCREATE(?: OR REPLACE)? FUNCTION\b", lines[i], re.I):
i += 1
continue
start = i
header = [lines[i]]
tag = None
j = i
while j + 1 < len(lines):
tag = dollar_at(lines[j])
if tag:
break
j += 1
header.append(lines[j])
if not tag:
i += 1
continue
body = []
k = j + 1
while k < len(lines) and tag not in lines[k]:
body.append(lines[k])
k += 1
body_text = "\n".join(body)
found = [t for t in targets if re.search(rf"\b{re.escape(t)}\s*\(", body_text, re.I)]
if found:
h = "\n".join(header)
nm = re.search(r"\bFUNCTION\s+((?:\w+\.)?\w+)\s*\(", h, re.I)
name = nm.group(1) if nm else "<unknown>"
security = "SECURITY DEFINER" if re.search(r"\bSECURITY\s+DEFINER\b", h, re.I) else "SECURITY INVOKER/default"
print(f"{path}:{start+1}: {name}: {security}; calls={','.join(found)}")
i = k + 1
print("--- non-function call sites ---")
for path in sorted(Path("supabase/migrations").glob("*.sql")):
lines = path.read_text(errors="replace").splitlines()
in_function = False
tag = None
for n, line in enumerate(lines, 1):
if re.search(r"\bCREATE(?: OR REPLACE)? FUNCTION\b", line, re.I):
in_function = True
tag = None
if in_function and tag is None:
tag = dollar_at(line)
if not in_function:
if any(re.search(rf"\b{re.escape(t)}\s*\(", line, re.I) for t in targets):
print(f"{path}:{n}: {line.strip()}")
elif tag and tag in line and n > 1:
in_function = False
tag = None
PYRepository: Japegomez/jugaMUS
Length of output: 6148
🏁 Script executed:
#!/bin/bash
set -eu
echo "--- policies and views containing the roster function ---"
rg -n -B 12 -A 18 \
"match_effective_roster_filled\s*\(" \
supabase/migrations/20260518190000_016_match_text_roster_slots.sql \
supabase/migrations/20260524230000_051_match_in_progress_roster_rules.sql \
supabase/migrations/20260723120000_070_tournament_cancel_no_bracket.sql \
supabase/migrations/20260723130000_071_tournament_lifecycle_planned_guard.sql \
supabase/migrations/20260728130000_082_cancel_all_tournament_matches.sql \
supabase/migrations/20260728150000_084_harden_tournament_cancellation.sql \
supabase/migrations/20260811000000_102_match_timers_tournament_league.sql
echo "--- grants on matches and direct roster-function grants ---"
rg -n -i \
"^(GRANT|REVOKE)|ON public\.matches|ON public\.match_effective_roster_filled|ON FUNCTION public\.match_effective_roster_filled" \
supabase/migrations \
| rg -i "matches|match_effective_roster_filled|^.*:(GRANT|REVOKE)"
echo "--- match_invitations RLS policies ---"
sed -n '1,60p' supabase/migrations/20260812100001_109_match_invitations.sqlRepository: Japegomez/jugaMUS
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -eu
echo "--- exact grants/revokes for relevant functions ---"
rg -n -i \
"^(GRANT|REVOKE).*FUNCTION public\.(match_effective_roster_filled|match_pending_invitations_filled|inviter_team_capacity_available)" \
supabase/migrations || true
echo "--- match_invitations RLS and permissions ---"
sed -n '1,58p' supabase/migrations/20260812100001_109_match_invitations.sql
echo "--- calls outside CREATE FUNCTION bodies ---"
python3 - <<'PY'
from pathlib import Path
import re
targets = re.compile(r"\b(?:public\.)?(?:match_effective_roster_filled|match_pending_invitations_filled)\s*\(", re.I)
create_fn = re.compile(r"\bCREATE(?:\s+OR\s+REPLACE)?\s+FUNCTION\b", re.I)
dollar = re.compile(r"\$[A-Za-z_0-9]*\$")
for path in sorted(Path("supabase/migrations").glob("*.sql")):
text = path.read_text(errors="replace")
spans = []
for m in create_fn.finditer(text):
opening = dollar.search(text, m.end())
if not opening:
continue
closing = dollar.search(text, opening.end())
if not closing:
continue
spans.append((m.start(), closing.end()))
mask = list(text)
for a, b in spans:
mask[a:b] = " " * (b-a)
outside = "".join(mask)
hits = list(targets.finditer(outside))
if hits:
for h in hits:
line = outside.count("\n", 0, h.start()) + 1
original_line = text.splitlines()[line-1].strip()
print(f"{path}:{line}: {original_line}")
print("--- policy blocks containing relevant calls ---")
for path in sorted(Path("supabase/migrations").glob("*.sql")):
text = path.read_text(errors="replace")
for m in re.finditer(r"\bCREATE\s+POLICY\b", text, re.I):
end = text.find(";", m.end())
block = text[m.start(): end if end >= 0 else len(text)]
if targets.search(block):
line = text.count("\n", 0, m.start()) + 1
print(f"{path}:{line}: { ' '.join(block.split()) }")
PYRepository: Japegomez/jugaMUS
Length of output: 3312
🏁 Script executed:
#!/bin/bash
set -eu
echo "--- callers of inviter_team_capacity_available ---"
rg -n -B 8 -A 45 \
"CREATE( OR REPLACE)? FUNCTION public\.(invite_friend_to_match|inviter_team_capacity_available)|inviter_team_capacity_available\s*\(" \
supabase/migrations/20260812100001_109_match_invitations.sql
echo "--- security headers for all invitation RPCs ---"
python3 - <<'PY'
from pathlib import Path
import re
text = Path("supabase/migrations/20260812100001_109_match_invitations.sql").read_text()
for m in re.finditer(r"CREATE(?: OR REPLACE)? FUNCTION\s+public\.([a-z_][a-z0-9_]*)\s*\(", text, re.I):
name = m.group(1)
if name not in {
"invite_friend_to_match",
"respond_match_invitation",
"cancel_match_invitation",
"list_my_match_invitations",
"list_match_invitations",
"inviter_team_capacity_available",
"match_pending_invitations_filled",
"match_effective_roster_filled",
}:
continue
end = text.find("AS $$", m.end())
header = text[m.start():end if end >= 0 else m.end()]
line = text.count("\n", 0, m.start()) + 1
mode = "SECURITY DEFINER" if re.search(r"\bSECURITY\s+DEFINER\b", header, re.I) else "SECURITY INVOKER/default"
print(f"{line}: {name}: {mode}")
PYRepository: Japegomez/jugaMUS
Length of output: 8184
Haz la ruta de cálculo SECURITY DEFINER. match_effective_roster_filled(UUID) sigue siendo SECURITY INVOKER y conserva EXECUTE para PUBLIC. Un cliente puede invocarla directamente, pero su llamada a match_pending_invitations_filled falla con permission denied for function match_pending_invitations_filled. Los llamadores internos detectados son SECURITY DEFINER, pero no cubren este acceso directo. Si solo concedes EXECUTE al auxiliar, la RLS de match_invitations puede subestimar el conteo para otros usuarios. Define match_effective_roster_filled como SECURITY DEFINER y revoca su ejecución directa para PUBLIC.
🤖 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 `@supabase/migrations/20260812100001_109_match_invitations.sql` around lines
480 - 481, Actualiza la función match_effective_roster_filled(UUID) para
ejecutarse como SECURITY DEFINER y revoca su privilegio EXECUTE para PUBLIC.
Mantén el acceso interno a match_pending_invitations_filled(UUID) mediante el
contexto del definidor, sin conceder acceso público directo a esa función
auxiliar.
| REVOKE SELECT ON public.profiles FROM authenticated; | ||
| GRANT SELECT ( | ||
| id, display_name, city, photo_url, notify_push, notify_on_join, | ||
| notify_on_match_change, notify_on_match_start, notify_on_match_edit, notify_on_match_cancel, | ||
| notify_on_result, notify_on_reminder, notify_on_reminder_24h, notify_on_reminder_2h, | ||
| notify_on_reminder_in_progress, notify_on_friend_request, notify_on_match_invitation, | ||
| role, status, created_at, updated_at | ||
| ) ON public.profiles TO authenticated; | ||
|
|
||
| REVOKE UPDATE ON public.profiles FROM authenticated; | ||
| GRANT UPDATE ( | ||
| display_name, phone_e164, city, photo_url, notify_push, notify_on_join, | ||
| notify_on_match_change, notify_on_match_start, notify_on_match_edit, notify_on_match_cancel, | ||
| notify_on_result, notify_on_reminder, notify_on_reminder_24h, notify_on_reminder_2h, | ||
| notify_on_reminder_in_progress, notify_on_friend_request, notify_on_match_invitation, | ||
| push_token | ||
| ) ON public.profiles TO authenticated; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Comprobar si las columnas legacy existen y localizar el último GRANT de columnas sobre profiles.
echo "=== definiciones y ALTER de notify_on_match_change / notify_on_reminder ==="
rg -n "notify_on_match_change|notify_on_reminder\b" --glob '*.sql' supabase
echo
echo "=== GRANT/REVOKE previos sobre profiles ==="
fd -e sql . supabase/migrations --exec rg -n -B 2 -A 10 "GRANT SELECT \(|GRANT UPDATE \(" {} \
| rg -n "profiles" -B 10 | head -80
echo
echo "=== columnas seleccionadas desde el cliente ==="
rg -n "badge_showcase" src/services/profiles.service.tsRepository: Japegomez/jugaMUS
Length of output: 13293
🏁 Script executed:
#!/bin/bash
set -e
echo "=== migración 099 ==="
cat -n supabase/migrations/20260810220000_099_badge_showcase_grants.sql
echo
echo "=== migración 110 ==="
cat -n supabase/migrations/20260812100002_110_remove_friend_and_invite_notifications.sql
echo
echo "=== definiciones de profiles y tipos ==="
rg -n -B 4 -A 8 "badge_showcase|notify_on_match_change|notify_on_reminder" supabase/migrations src/types/database.types.ts
echo
echo "=== uso completo de profiles.service.ts ==="
cat -n src/services/profiles.service.ts | sed -n '70,90p'Repository: Japegomez/jugaMUS
Length of output: 50373
🏁 Script executed:
#!/bin/bash
python3 - <<'PY'
from pathlib import Path
import re
migration = Path("supabase/migrations/20260812100002_110_remove_friend_and_invite_notifications.sql").read_text()
previous = Path("supabase/migrations/20260810220000_099_badge_showcase_grants.sql").read_text()
service = Path("src/services/profiles.service.ts").read_text()
def grant_columns(text, kind):
match = re.search(rf"GRANT {kind} \((.*?)\) ON public\.profiles", text, re.S)
return {c.strip() for c in match.group(1).split(",")} if match else set()
select_110 = grant_columns(migration, "SELECT")
update_110 = grant_columns(migration, "UPDATE")
select_099 = grant_columns(previous, "SELECT")
update_099 = grant_columns(previous, "UPDATE")
query = re.search(r"'([^']*badge_showcase[^']*)'", service).group(1)
query_columns = {c.strip() for c in query.split(",")}
print("110 SELECT includes badge_showcase:", "badge_showcase" in select_110)
print("110 UPDATE includes badge_showcase:", "badge_showcase" in update_110)
print("099 SELECT included badge_showcase:", "badge_showcase" in select_099)
print("099 UPDATE included badge_showcase:", "badge_showcase" in update_099)
print("service fields missing from 110 SELECT:", sorted(query_columns - select_110))
print("legacy columns added before 110:",
"notify_on_match_change" in Path("supabase/migrations/20260725120000_080_pr_review_followups.sql").read_text(),
"notify_on_reminder" in Path("supabase/migrations/20260725120000_080_pr_review_followups.sql").read_text())
print("110 legacy columns in SELECT/UPDATE:",
"notify_on_match_change" in select_110,
"notify_on_reminder" in select_110,
"notify_on_match_change" in update_110,
"notify_on_reminder" in update_110)
PY
echo
echo "=== contexto de profiles.service.ts ==="
cat -n src/services/profiles.service.ts | sed -n '68,88p'Repository: Japegomez/jugaMUS
Length of output: 2543
Incluye badge_showcase en los GRANT de profiles. La migración 110 lo elimina de GRANT SELECT y GRANT UPDATE, aunque la migración 099 lo concedía. La consulta de profiles.service.ts:82 fallará con permission denied for column badge_showcase, y las actualizaciones de esta columna también fallarán. Las columnas notify_on_match_change y notify_on_reminder sí existen.
🤖 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
`@supabase/migrations/20260812100002_110_remove_friend_and_invite_notifications.sql`
around lines 9 - 25, Update the profile privileges in the migration around the
GRANT SELECT and GRANT UPDATE statements to include the existing badge_showcase
column in both column lists. Preserve the other granted columns, including
notify_on_match_change and notify_on_reminder.
Address review findings for friends/match invites: capacity and auth on RPCs, create/detail invite flow, profile privacy, docs, and tests (mig. 111-116). Co-authored-by: Cursor <cursoragent@cursor.com>
|
@coderabbitai full review |
|
There was a problem hiding this comment.
Actionable comments posted: 9
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.
🟡 Other comments (7)
src/hooks/useExploreListsRealtimeSync.ts-85-90 (1)
85-90: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winConserva todos los identificadores de partida pendientes.
pendingRef.current.matchIdconserva solo el últimomatch_id. Si llegan eventos dematch_invitationspara partidas distintas durante los 300 ms de debounce, las líneas 61-65 invalidan únicamente las consultas específicas de la última partida. Usa una colección dematchIdy ejecuta las invalidaciones por cada identificador. Añade una prueba para dos eventos consecutivos de partidas distintas.🤖 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 `@src/hooks/useExploreListsRealtimeSync.ts` around lines 85 - 90, Actualiza el manejo de eventos en useExploreListsRealtimeSync para conservar todos los matchId pendientes durante el debounce, reemplazando el valor único de pendingRef.current.matchId por una colección. Modifica la lógica de invalidación de las líneas 61-65 para recorrer cada identificador y ejecutar sus consultas específicas, limpiando la colección después; añade una prueba que cubra dos eventos consecutivos de match_invitations con partidas distintas.src/utils/leagueFixtures.test.ts-35-40 (1)
35-40: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winComprueba la correspondencia uno a uno de la segunda vuelta.
first.find(...)solo verifica que cada fixture desecondtenga algún espejo enfirst. No compara multiplicidades. Sisecondrepite una pareja válida y omite otra, la prueba puede pasar porque solo valida la longitud. Compara las listas normalizadas de parejas invertidas.Propuesta de corrección
- for (const leg of second) { - const mirror = first.find((f) => f.pairAId === leg.pairBId && f.pairBId === leg.pairAId) - expect(mirror).toBeDefined() - } + const expectedSecond = first + .map((f) => `${f.pairBId}|${f.pairAId}`) + .sort() + const actualSecond = second + .map((f) => `${f.pairAId}|${f.pairBId}`) + .sort() + expect(actualSecond).toEqual(expectedSecond)🤖 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 `@src/utils/leagueFixtures.test.ts` around lines 35 - 40, Actualiza la prueba que separa los fixtures first y second para comparar listas normalizadas de parejas: transforma cada fixture de second en su pareja invertida y compárala con la lista normalizada de first, preservando duplicados y ordenando de forma determinista. Sustituye la validación basada en first.find dentro del bucle por una aserción de igualdad entre ambas listas, manteniendo la comprobación de longitudes solo si sigue siendo necesaria.supabase/migrations/20260812150000_114_review_followups_friends_invites.sql-48-54 (1)
48-54: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winEl tipo de notificación no corresponde al evento.
La notificación de las líneas 48-54 informa de una solicitud aceptada, pero usa
p_type := 'friend_request_received'. El filtro de preferencias del usuario aplicará la clave de solicitudes recibidas a un evento de aceptación. Un usuario que desactive las solicitudes de amistad tampoco recibirá el aviso de que su propia solicitud fue aceptada.Use un tipo específico, por ejemplo
friend_request_accepted, y añada su tratamiento ensupabase/functions/process-notifications/index.ts.🤖 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 `@supabase/migrations/20260812150000_114_review_followups_friends_invites.sql` around lines 48 - 54, Update the acceptance notification in enqueue_notification to use the dedicated friend_request_accepted type instead of friend_request_received, and add corresponding handling for this type in process-notifications so preference filtering and delivery work correctly.src/components/profile/FriendsSection.tsx-98-109 (1)
98-109: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winEl drawer puede quedar abierto si la animación se interrumpe.
startdevuelvefinished: falsecuando otra animación toma el control del valor o cuando se llamastopAnimation. En ese caso la función retorna antes desetOpen(false),clearDrawerState()yonClosed?.(). El drawer permanece visible, el texto de búsqueda no se limpia y la navegación al perfil o la apertura del modal de invitación no ocurren.Ejecute siempre el cierre de estado y reserve la condición solo para el caso interrumpido si desea conservar la posición visual.
🛡️ Corrección propuesta
const animateDrawerClosed = (onClosed?: () => void) => { Animated.timing(slide, { toValue: PANEL_WIDTH, duration: 200, useNativeDriver: true, - }).start(({ finished }) => { - if (!finished) return + }).start(() => { clearDrawerState() setOpen(false) onClosed?.() }) }🤖 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 `@src/components/profile/FriendsSection.tsx` around lines 98 - 109, Update animateDrawerClosed so clearDrawerState(), setOpen(false), and onClosed?.() always execute after the animation callback, including when finished is false; only use the finished condition to preserve any required visual-position behavior rather than returning before state cleanup and follow-up actions.src/services/results.service.ts-16-18 (1)
16-18: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCorrige el mensaje de
has_pending_rival_invites.
mapResultRpcErrortraduce errores de rechazo del RPC. Cuandorecord_match_result_directdevuelvehas_pending_rival_invites, no se registró ningún resultado. El texto actual dice «El resultado quedará a la espera de validación del rival», en futuro afirmativo, y sugiere que la operación se completó. El usuario puede dar el marcador por guardado y no reintentar.Indica el rechazo y la acción correcta, igual que hace el mensaje de
has_other_participants.🐛 Corrección propuesta
if (message.includes('has_pending_rival_invites')) { - return 'Hay invitaciones pendientes. El resultado quedará a la espera de validación del rival.' + return 'Hay invitaciones pendientes. Usa el flujo de validación de resultado para que el rival valide el marcador.' }🤖 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 `@src/services/results.service.ts` around lines 16 - 18, Actualiza el mensaje devuelto por mapResultRpcError para has_pending_rival_invites, dejando claro que el resultado fue rechazado y que el usuario debe esperar la validación del rival antes de reintentarlo, siguiendo el tono y la instrucción accionable de has_other_participants.src/app/(tabs)/matches/create.tsx-408-430 (1)
408-430: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winEvita el cierre directo con una invitación pendiente del equipo A
record_match_result_directrechaza cualquier invitaciónpendingoacceptedcuyoinvitee_idno sea el creador, sin comprobarmi.team. Por tanto, con una invitación A creada yinvitesB.length === 0, la rama directa lanzahas_pending_rival_invitesdespués de crear la partida y la invitación. Limita esta guarda a invitaciones del equipo rival.🤖 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 `@src/app/`(tabs)/matches/create.tsx around lines 408 - 430, Update the direct result-recording flow around submitResult.mutateAsync and recordMatchResult.mutateAsync so it only falls back to the direct path when there are no pending or accepted rival-team invitations; an invitation for team A must not qualify as a rival invite. Reuse the existing team/invitation data to ensure the guard filters by the rival team before evaluating invitesB.length or invoking recordMatchResult.supabase/migrations/20260812170000_116_friend_request_status_and_profile_phone.sql-31-35 (1)
31-35: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winManejar el conflicto de unicidad en solicitudes simultáneas
La migración 108 define
uniq_friendships_pairsobre el par normalizado.FOR UPDATEno bloquea filas inexistentes. Dos llamadas concurrentes pueden pasarIF NOT FOUND; una inserta y la otra propagaunique_violation. Captura el conflicto, vuelve a leer la fila y aplicaalready_friendsorequest_already_pending.🤖 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 `@supabase/migrations/20260812170000_116_friend_request_status_and_profile_phone.sql` around lines 31 - 35, Actualiza la función que consulta friendships con FOR UPDATE para manejar concurrencia sobre filas inexistentes: captura unique_violation durante la inserción, vuelve a leer el registro normalizado y devuelve already_friends o request_already_pending según su estado, en lugar de propagar la excepción. Mantén el flujo existente para filas encontradas y usa la restricción uniq_friendships_pair como garantía de unicidad.
🧹 Nitpick comments (11)
src/hooks/useMatches.ts (1)
36-36: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winElimina el ciclo entre los hooks.
src/hooks/useMatches.tsimportainvalidateMatchInvitationQueries, mientrassrc/hooks/useMatchInvitations.tsimporta símbolos desdeuseMatches. Extrae las query keys y los helpers compartidos a un módulo sin hooks. Las exportaciones actuales son declaracionesfunction, por lo que el ciclo no produce un error TDZ durante la evaluación actual, pero aumenta el acoplamiento entre módulos.🤖 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 `@src/hooks/useMatches.ts` at line 36, Elimina el ciclo entre useMatches y useMatchInvitations extrayendo las query keys y helpers compartidos de invalidateMatchInvitationQueries y los símbolos importados desde useMatches a un módulo independiente sin hooks. Actualiza ambos hooks para importar esos símbolos desde el nuevo módulo y conserva las exportaciones públicas existentes.supabase/migrations/20260812150000_114_review_followups_friends_invites.sql (1)
96-100: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueAñada la restricción como
NOT VALIDy valídela después.
ADD CONSTRAINT ... CHECKfuerza un escaneo de tabla y bloquea las escrituras durante el escaneo. La tablafriendshipses nueva en este release, por lo que el impacto actual es bajo, pero el patrón seguro evita el bloqueo en futuras re-aplicaciones sobre datos existentes.♻️ Refactor propuesto
ALTER TABLE public.friendships ADD CONSTRAINT friendships_message_len_chk - CHECK (message IS NULL OR char_length(message) <= 200); + CHECK (message IS NULL OR char_length(message) <= 200) NOT VALID; +ALTER TABLE public.friendships + VALIDATE CONSTRAINT friendships_message_len_chk;🤖 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 `@supabase/migrations/20260812150000_114_review_followups_friends_invites.sql` around lines 96 - 100, Update the friendships_message_len_chk migration to add the CHECK constraint with NOT VALID, then explicitly validate it afterward using VALIDATE CONSTRAINT. Preserve the existing message-null and 200-character condition and constraint replacement behavior.Source: Linters/SAST tools
supabase/migrations/20260812130000_112_list_my_match_invitations_exclude_cancelled.sql (1)
10-10: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winRenombre la columna
statusamatch_status.La columna de salida
statuscontienem.status(estado de la partida), nomi.status(estado de la invitación). La tablamatch_invitationstambién tiene una columnastatus, por lo que el nombre invita a una lectura incorrecta en el cliente. El comentario de la línea 2 lo documenta, pero el nombre sigue siendo ambiguo.Renombre la columna a
match_statusy actualiceMyMatchInvitationRowensrc/services/matchInvitations.service.tsysrc/types/database.types.ts.Also applies to: 26-26
🤖 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 `@supabase/migrations/20260812130000_112_list_my_match_invitations_exclude_cancelled.sql` at line 10, Rename the output column status to match_status in the migration’s match invitation query, preserving its value from m.status. Update the corresponding MyMatchInvitationRow definition in matchInvitations.service.ts and the generated database type in database.types.ts so all consumers use match_status consistently.src/components/profile/FriendsSection.tsx (1)
483-483: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDiferencie el estado de carga entre aceptar y rechazar.
busyIdsse indexa porfriendship_id, por lo que ambos botones muestran el indicador de carga durante cualquiera de las dos acciones. El usuario no puede saber qué acción está en curso.Guarde también la acción en el estado, por ejemplo
Map<string, 'accept' | 'reject'>, y active el indicador solo en el botón correspondiente.Also applies to: 497-497
🤖 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 `@src/components/profile/FriendsSection.tsx` at line 483, Update the loading state around the accept/reject controls in FriendsSection to store the active action per friendship_id, using distinct accept and reject values. Make each button’s loading indicator active only when its corresponding action is recorded, while preserving the existing busy-state lifecycle.src/app/(tabs)/matches/create.tsx (4)
344-347: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueEsta comprobación es inalcanzable.
Las líneas 312-315 ya retornan cuando
isPastResultModeestrueypastResultes nulo. Al llegar a la línea 344,pastResultno puede ser nulo en modo de partida pasada, así que la condición nunca se cumple.♻️ Refactor propuesto
- if (isPastResultMode && invitesB.length > 0 && !pastResult) { - showAlert('Resultado pendiente', 'Selecciona el marcador de la partida antes de crearla.') - return - }🤖 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 `@src/app/`(tabs)/matches/create.tsx around lines 344 - 347, Elimina la comprobación inalcanzable de `pastResult` dentro del bloque que valida `isPastResultMode` e `invitesB`, ya que el retorno previo alrededor de las líneas 312-315 ya cubre ese caso. Mantén intactas las demás validaciones y el flujo de creación de la partida.
265-271: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSimplifica el ternario redundante de
team_b_player_1.Las ramas
invitesB.length === 1y la rama final devuelven la misma expresiónteamBPlayer1?.trim() || null. La condición intermedia no cambia el resultado.♻️ Refactor propuesto
- team_b_player_1: - invitesB.length >= 2 - ? null - : invitesB.length === 1 - ? teamBPlayer1?.trim() || null - : teamBPlayer1?.trim() || null, + team_b_player_1: invitesB.length >= 2 ? null : teamBPlayer1?.trim() || null,🤖 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 `@src/app/`(tabs)/matches/create.tsx around lines 265 - 271, Simplifica la asignación de team_b_player_1 eliminando el ternario intermedio redundante: conserva únicamente la comprobación invitesB.length >= 2 para devolver null y reutiliza teamBPlayer1?.trim() || null como resultado alternativo.
302-308: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueElimina los dos efectos de recorte de invitaciones.
inviteCapacityAeinviteCapacityBson constantes literales (líneas 220-221) con valores 1 y 2. Sus valores nunca cambian entre renders, así que ambosuseEffectse ejecutan una única vez al montar, cuandoinvitesAeinvitesBestán vacíos. Nunca recortan nada.
toggleInviteya impone el límite (líneas 229 y 237) yonSubmitaplicaslicesobre las capacidades (líneas 357-358). Los efectos son código muerto.♻️ Refactor propuesto
- useEffect(() => { - setInvitesA((prev) => (prev.length > inviteCapacityA ? prev.slice(0, inviteCapacityA) : prev)) - }, [inviteCapacityA]) - - useEffect(() => { - setInvitesB((prev) => (prev.length > inviteCapacityB ? prev.slice(0, inviteCapacityB) : prev)) - }, [inviteCapacityB]) -🤖 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 `@src/app/`(tabs)/matches/create.tsx around lines 302 - 308, Elimina los dos useEffect que recortan invitesA e invitesB según inviteCapacityA e inviteCapacityB. Conserva intacta la lógica existente de toggleInvite y onSubmit, que ya impone las capacidades mediante sus límites y slice.
225-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSaca las llamadas a
setValuefuera de los updaters de estado.
setInvitesAysetInvitesBdeben usar updaters puros. React puede ejecutarlos más de una vez con Strict Mode. Conserva los updaters funcionales para que las llamadas consecutivas usen el estado más reciente. La lógica basada directamente eninvitesAoinvitesBpuede perder selecciones antes del siguiente render.🤖 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 `@src/app/`(tabs)/matches/create.tsx around lines 225 - 250, Mueve todas las llamadas a setValue fuera de los updaters de setInvitesA y setInvitesB en toggleInvite, manteniendo esos updaters puros. Calcula las acciones de limpieza usando el estado más reciente antes de actualizar las invitaciones, sin depender de invitesA o invitesB capturados del render, y conserva la lógica de selección y capacidad actual.supabase/migrations/20260812120000_111_search_users_by_display_name.sql (1)
53-59: 🚀 Performance & Scalability | 🔵 TrivialAñade un índice trigram para la búsqueda por subcadena.
El patrón
ILIKE '%...%'con comodín inicial no aprovecha índices B-tree. Cada búsqueda provoca un escaneo secuencial depublic.profilesfiltrado porstatus = 'active'. El coste crece linealmente con el número de perfiles y la función se invoca en cada pulsación de la búsqueda de amigos.Considera habilitar
pg_trgmy crear un índice GIN sobrelower(display_name)para los perfiles activos.CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX IF NOT EXISTS profiles_display_name_trgm_idx ON public.profiles USING gin (lower(display_name) gin_trgm_ops) WHERE status = 'active';🤖 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 `@supabase/migrations/20260812120000_111_search_users_by_display_name.sql` around lines 53 - 59, Enable the pg_trgm extension and add the partial GIN index profiles_display_name_trgm_idx on lower(public.profiles.display_name), restricted to rows where status = 'active'. Place the migration changes alongside the search query using p.display_name ILIKE so substring searches can use the index.src/components/matches/ShareMatchInviteModal.tsx (1)
40-48: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueCancela el temporizador de
copiedal desmontar.
setTimeoutprogramasetCopied(false)a los 2 segundos sin guardar el identificador. Si el usuario copia la invitación y sale de la pantalla antes de que expire, la actualización de estado se ejecuta sobre un componente desmontado.Guarda el identificador en una
refy límpialo en un efecto de desmontaje.♻️ Refactor propuesto
-import { useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react'const [copied, setCopied] = useState(false) const [sharing, setSharing] = useState(false) + const copiedTimer = useRef<ReturnType<typeof setTimeout> | null>(null) + + useEffect( + () => () => { + if (copiedTimer.current) clearTimeout(copiedTimer.current) + }, + [] + )const handleCopy = async () => { try { await Clipboard.setStringAsync(shareMessage) setCopied(true) - setTimeout(() => setCopied(false), 2000) + if (copiedTimer.current) clearTimeout(copiedTimer.current) + copiedTimer.current = setTimeout(() => setCopied(false), 2000) } catch {🤖 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 `@src/components/matches/ShareMatchInviteModal.tsx` around lines 40 - 48, Actualiza handleCopy para guardar el identificador de setTimeout en una ref y añade un efecto de desmontaje que lo cancele con clearTimeout. Conserva el reinicio de copied después de 2 segundos y evita actualizar el estado cuando ShareMatchInviteModal ya no esté montado.src/hooks/useMatchInvitations.ts (1)
100-106: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winInvalida también las invitaciones del partido al responder.
onSuccessinvalida las invitaciones del usuario y el dashboard, pero nomatchInvitationsQueryKey(matchId). La pantalla de detalle consume esa consulta ensrc/app/(tabs)/matches/[id].tsx(línea 589) para calcular las plazas mostradas (displaySlotsA,displaySlotsB) y las filas «Pendiente». Después de aceptar o rechazar, la lista de invitaciones del partido puede quedar obsoleta hasta que expireQUERY_STALE_TIME.Reutiliza
invalidateMatchInvitationQueries(líneas 26-38), que ya invalida las tres claves. Así se evita también la invalidación amplia de['match'], que afecta a todas las partidas en caché.♻️ Refactor propuesto
onSuccess: (_data, variables) => { - if (userId) { - queryClient.invalidateQueries({ queryKey: myMatchInvitationsQueryKey(userId) }) - invalidateMyMatchesDashboard(queryClient, userId) - } - queryClient.invalidateQueries({ queryKey: ['match'] }) + invalidateMatchInvitationQueries(queryClient, { + userId, + matchId: variables.matchId, + }) + if (userId) { + invalidateMyMatchesDashboard(queryClient, userId) + } },🤖 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 `@src/hooks/useMatchInvitations.ts` around lines 100 - 106, Actualiza el callback onSuccess de la mutación para reutilizar invalidateMatchInvitationQueries, asegurando la invalidación de las consultas del partido y del usuario tras responder una invitación. Elimina la invalidación amplia de ['match'] y conserva la invalidación del dashboard mediante invalidateMyMatchesDashboard cuando exista userId.
🤖 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 `@src/app/`(tabs)/matches/[id].tsx:
- Line 831: Actualiza el cálculo de rivalHasPendingInvites para seleccionar las
invitaciones del equipo rival según myParticipation?.team, en lugar de asumir
siempre el equipo B. Mantén la lógica alineada con
rivalHasRegisteredParticipants y asegúrate de que rivalAutoConfirms refleje
correctamente el estado real del rival antes de pasar datos a SubmitResultModal.
- Around line 713-720: Update the personal-match classification around
hasOutstandingAccountInvites and the effect that opens RecordResultModal so it
waits for useMatchInvitations(id) to finish before evaluating isPersonalMatch.
Do not treat undefined matchInvitations as an empty list; preserve the existing
pending-invite check once data is available, and keep the modal closed while the
invitation query is loading.
In `@src/app/`(tabs)/matches/create.tsx:
- Around line 356-359: Remove the invalid `as const` assertions from the
`TEAM.A` and `TEAM.B` mappings in `inviteTargets`, and explicitly type the array
or mapped objects so the `team` property retains the expected `TEAM` type.
In `@src/components/matches/EditMatchTeamModal.tsx`:
- Line 223: Corrige el cálculo de remainingSlots para que freeSlots sea la única
fuente del número de plazas disponibles y no vuelva a descontar
invitedIds.length. Conserva únicamente la limitación necesaria para impedir
invitaciones duplicadas usando invitedIds, tanto en la lógica de la línea
indicada como en el bloque equivalente señalado en 250-252.
In `@supabase/migrations/20260812120000_111_search_users_by_display_name.sql`:
- Line 24: Escapa en la función de búsqueda el contenido de v_q antes de
interpolarlo en el patrón ILIKE, neutralizando los comodines % y _ y cualquier
carácter de escape existente; declara explícitamente el carácter ESCAPE en la
comparación ILIKE. Aplica el mismo ajuste a la construcción equivalente del
patrón en la otra ubicación señalada, preservando la validación de longitud y el
límite de resultados.
In `@supabase/migrations/20260812125724_115_review_followups_invites_auth.sql`:
- Around line 1-2: Corrige la migración identificada por el encabezado “115”
para que su nombre tenga un prefijo posterior a 20260812150000, o elimínala si
la migración 114 ya establece el estado requerido. Actualiza también la cabecera
para documentar el orden lexicográfico real de aplicación y evitar que 115 se
ejecute antes de 112–114.
In
`@supabase/migrations/20260812140000_113_void_confirmed_result_on_invitation_reject.sql`:
- Around line 64-67: Actualiza la función respond_match_invitation en esta
migración: añade FOR UPDATE al SELECT de v_inv y valida
inviter_team_capacity_available antes de insertar al participante, igual que en
las migraciones 114 y 115. Mantén las comprobaciones existentes de invitación y
conserva el rechazo cuando el equipo no tenga capacidad.
In `@supabase/migrations/20260812150000_114_review_followups_friends_invites.sql`:
- Around line 4-8: Corrige el contrato de send_friend_request para devolver
TABLE (friendship_id UUID, status TEXT) y retornar ambos valores con el estado
real en sus dos ramas. En
supabase/migrations/20260812150000_114_review_followups_friends_invites.sql,
actualiza la firma y los retornos; en src/types/database.types.ts:2205-2219,
regenera los tipos con las migraciones 111-116; y en
src/services/friends.service.ts:63-87, elimina la compatibilidad con respuestas
cadena y consume directamente friendship_id y status.
In
`@supabase/migrations/20260812170000_116_friend_request_status_and_profile_phone.sql`:
- Around line 110-130: Actualiza la función get_profile_with_phone para eliminar
push_token y todas las columnas notify_* tanto de RETURNS TABLE como de su
SELECT, conservando únicamente los campos usados por ParticipantProfile.
Regenera src/types/database.types.ts y ajusta los consumidores de
get_profile_with_phone para usar el nuevo contrato sin acceder a esos campos.
---
Other comments:
In `@src/app/`(tabs)/matches/create.tsx:
- Around line 408-430: Update the direct result-recording flow around
submitResult.mutateAsync and recordMatchResult.mutateAsync so it only falls back
to the direct path when there are no pending or accepted rival-team invitations;
an invitation for team A must not qualify as a rival invite. Reuse the existing
team/invitation data to ensure the guard filters by the rival team before
evaluating invitesB.length or invoking recordMatchResult.
In `@src/components/profile/FriendsSection.tsx`:
- Around line 98-109: Update animateDrawerClosed so clearDrawerState(),
setOpen(false), and onClosed?.() always execute after the animation callback,
including when finished is false; only use the finished condition to preserve
any required visual-position behavior rather than returning before state cleanup
and follow-up actions.
In `@src/hooks/useExploreListsRealtimeSync.ts`:
- Around line 85-90: Actualiza el manejo de eventos en
useExploreListsRealtimeSync para conservar todos los matchId pendientes durante
el debounce, reemplazando el valor único de pendingRef.current.matchId por una
colección. Modifica la lógica de invalidación de las líneas 61-65 para recorrer
cada identificador y ejecutar sus consultas específicas, limpiando la colección
después; añade una prueba que cubra dos eventos consecutivos de
match_invitations con partidas distintas.
In `@src/services/results.service.ts`:
- Around line 16-18: Actualiza el mensaje devuelto por mapResultRpcError para
has_pending_rival_invites, dejando claro que el resultado fue rechazado y que el
usuario debe esperar la validación del rival antes de reintentarlo, siguiendo el
tono y la instrucción accionable de has_other_participants.
In `@src/utils/leagueFixtures.test.ts`:
- Around line 35-40: Actualiza la prueba que separa los fixtures first y second
para comparar listas normalizadas de parejas: transforma cada fixture de second
en su pareja invertida y compárala con la lista normalizada de first,
preservando duplicados y ordenando de forma determinista. Sustituye la
validación basada en first.find dentro del bucle por una aserción de igualdad
entre ambas listas, manteniendo la comprobación de longitudes solo si sigue
siendo necesaria.
In `@supabase/migrations/20260812150000_114_review_followups_friends_invites.sql`:
- Around line 48-54: Update the acceptance notification in enqueue_notification
to use the dedicated friend_request_accepted type instead of
friend_request_received, and add corresponding handling for this type in
process-notifications so preference filtering and delivery work correctly.
In
`@supabase/migrations/20260812170000_116_friend_request_status_and_profile_phone.sql`:
- Around line 31-35: Actualiza la función que consulta friendships con FOR
UPDATE para manejar concurrencia sobre filas inexistentes: captura
unique_violation durante la inserción, vuelve a leer el registro normalizado y
devuelve already_friends o request_already_pending según su estado, en lugar de
propagar la excepción. Mantén el flujo existente para filas encontradas y usa la
restricción uniq_friendships_pair como garantía de unicidad.
---
Nitpick comments:
In `@src/app/`(tabs)/matches/create.tsx:
- Around line 344-347: Elimina la comprobación inalcanzable de `pastResult`
dentro del bloque que valida `isPastResultMode` e `invitesB`, ya que el retorno
previo alrededor de las líneas 312-315 ya cubre ese caso. Mantén intactas las
demás validaciones y el flujo de creación de la partida.
- Around line 265-271: Simplifica la asignación de team_b_player_1 eliminando el
ternario intermedio redundante: conserva únicamente la comprobación
invitesB.length >= 2 para devolver null y reutiliza teamBPlayer1?.trim() || null
como resultado alternativo.
- Around line 302-308: Elimina los dos useEffect que recortan invitesA e
invitesB según inviteCapacityA e inviteCapacityB. Conserva intacta la lógica
existente de toggleInvite y onSubmit, que ya impone las capacidades mediante sus
límites y slice.
- Around line 225-250: Mueve todas las llamadas a setValue fuera de los updaters
de setInvitesA y setInvitesB en toggleInvite, manteniendo esos updaters puros.
Calcula las acciones de limpieza usando el estado más reciente antes de
actualizar las invitaciones, sin depender de invitesA o invitesB capturados del
render, y conserva la lógica de selección y capacidad actual.
In `@src/components/matches/ShareMatchInviteModal.tsx`:
- Around line 40-48: Actualiza handleCopy para guardar el identificador de
setTimeout en una ref y añade un efecto de desmontaje que lo cancele con
clearTimeout. Conserva el reinicio de copied después de 2 segundos y evita
actualizar el estado cuando ShareMatchInviteModal ya no esté montado.
In `@src/components/profile/FriendsSection.tsx`:
- Line 483: Update the loading state around the accept/reject controls in
FriendsSection to store the active action per friendship_id, using distinct
accept and reject values. Make each button’s loading indicator active only when
its corresponding action is recorded, while preserving the existing busy-state
lifecycle.
In `@src/hooks/useMatches.ts`:
- Line 36: Elimina el ciclo entre useMatches y useMatchInvitations extrayendo
las query keys y helpers compartidos de invalidateMatchInvitationQueries y los
símbolos importados desde useMatches a un módulo independiente sin hooks.
Actualiza ambos hooks para importar esos símbolos desde el nuevo módulo y
conserva las exportaciones públicas existentes.
In `@src/hooks/useMatchInvitations.ts`:
- Around line 100-106: Actualiza el callback onSuccess de la mutación para
reutilizar invalidateMatchInvitationQueries, asegurando la invalidación de las
consultas del partido y del usuario tras responder una invitación. Elimina la
invalidación amplia de ['match'] y conserva la invalidación del dashboard
mediante invalidateMyMatchesDashboard cuando exista userId.
In `@supabase/migrations/20260812120000_111_search_users_by_display_name.sql`:
- Around line 53-59: Enable the pg_trgm extension and add the partial GIN index
profiles_display_name_trgm_idx on lower(public.profiles.display_name),
restricted to rows where status = 'active'. Place the migration changes
alongside the search query using p.display_name ILIKE so substring searches can
use the index.
In
`@supabase/migrations/20260812130000_112_list_my_match_invitations_exclude_cancelled.sql`:
- Line 10: Rename the output column status to match_status in the migration’s
match invitation query, preserving its value from m.status. Update the
corresponding MyMatchInvitationRow definition in matchInvitations.service.ts and
the generated database type in database.types.ts so all consumers use
match_status consistently.
In `@supabase/migrations/20260812150000_114_review_followups_friends_invites.sql`:
- Around line 96-100: Update the friendships_message_len_chk migration to add
the CHECK constraint with NOT VALID, then explicitly validate it afterward using
VALIDATE CONSTRAINT. Preserve the existing message-null and 200-character
condition and constraint replacement behavior.
🪄 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: Organization UI
Review profile: QUIET
Plan: Pro Plus
Run ID: 83202d6a-210c-4d0a-bb32-10dfb964d0d1
📒 Files selected for processing (36)
.github/workflows/eas.ymlREADME.mdREQUIREMENTS.mdTASKS.mdsrc/app/(tabs)/matches/[id].tsxsrc/app/(tabs)/matches/create.tsxsrc/app/(tabs)/matches/index.tsxsrc/app/(tabs)/profile/[userId].tsxsrc/app/(tabs)/profile/index.tsxsrc/components/matches/EditMatchTeamModal.tsxsrc/components/matches/ShareMatchInviteModal.tsxsrc/components/matches/SubmitResultModal.tsxsrc/components/profile/FriendsSection.tsxsrc/hooks/useExploreListsRealtimeSync.tssrc/hooks/useFriends.tssrc/hooks/useMatchInvitations.tssrc/hooks/useMatches.tssrc/lib/analytics.tssrc/lib/invalidateExploreCaches.test.tssrc/lib/realtimeRowIds.tssrc/services/friends.service.test.tssrc/services/friends.service.tssrc/services/matchInvitations.service.test.tssrc/services/matchInvitations.service.tssrc/services/profiles.service.tssrc/services/results.service.tssrc/types/database.types.tssrc/utils/leagueFixtures.test.tssrc/utils/notificationPrefs.test.tssrc/utils/notificationPrefs.tssupabase/migrations/20260812120000_111_search_users_by_display_name.sqlsupabase/migrations/20260812125724_115_review_followups_invites_auth.sqlsupabase/migrations/20260812130000_112_list_my_match_invitations_exclude_cancelled.sqlsupabase/migrations/20260812140000_113_void_confirmed_result_on_invitation_reject.sqlsupabase/migrations/20260812150000_114_review_followups_friends_invites.sqlsupabase/migrations/20260812170000_116_friend_request_status_and_profile_phone.sql
Also land invite/review follow-ups (mig. 117, query keys, rival invite guards) and align docs. Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve PR #158 conflicts keeping v1.8.0 and invite docs; retain coverage gates and characterization tests from main. Co-authored-by: Cursor <cursoragent@cursor.com>
Restore testing-library lock entries from main and pin react-test-renderer to 19.1.0 to match Expo React. Co-authored-by: Cursor <cursoragent@cursor.com>
Characterization tests for useFriends, useMatchInvitations, matchQueryKeys and municipalities so test:ci clears the 60% thresholds after v1.8. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep develop (v1.8.1 + review follow-ups) where the squash of #158 overlapped. Co-authored-by: Cursor <cursoragent@cursor.com>
Release v1.8.0 (
develop→main)Promueve a producción la versión 1.8.0 y el resto de cambios acumulados en
developdesde el último merge amain(hotfix v1.7.1).Destacado — Amigos e invitaciones a partidas (v1.8)
108,110)109)pending_validationcon rivales invitados; rechazo cancela partida iniciada/finalizadanotify_on_friend_request/notify_on_match_invitation(perfil +enqueue_notification+ Edge Functionprocess-notifications)friend_request_sent,match_invite_sent,match_invite_acceptedapp.json,package.json)También incluido desde
develop(post–v1.7.1 en main)GITHUB_TOKENREQUIREMENTS.md,TASKS.md)Checklist previo al merge
108–110(y pendientes dedevelop) en Supabase producciónprocess-notificationsactualizadadatabase.types.tssi hace falta tras aplicar migracionesNotas
develop)main, el workflow EAS debería etiquetarv1.8.0-…y lanzar build/submit.Summary by CodeRabbit