Skip to content

release: v1.8.0 — amigos e invitaciones a partidas - #158

Merged
Japegomez merged 21 commits into
mainfrom
develop
Aug 12, 2026
Merged

release: v1.8.0 — amigos e invitaciones a partidas#158
Japegomez merged 21 commits into
mainfrom
develop

Conversation

@Japegomez

@Japegomez Japegomez commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Release v1.8.0 (developmain)

Promueve a producción la versión 1.8.0 y el resto de cambios acumulados en develop desde el último merge a main (hotfix v1.7.1).

Destacado — Amigos e invitaciones a partidas (v1.8)

  • Sistema de amigos: solicitudes con mensaje, aceptar/rechazar/cancelar, eliminar amistad (mig. 108, 110)
  • Invitaciones a partidas (pareja/rival) al crear o editar equipo; pendientes ocupan plaza de roster (mig. 109)
  • Mis Partidas: sección Invitaciones; ficha: banner Aceptar/Rechazar + deeplink WhatsApp
  • Resultado pending_validation con rivales invitados; rechazo cancela partida iniciada/finalizada
  • Preferencias push: notify_on_friend_request / notify_on_match_invitation (perfil + enqueue_notification + Edge Function process-notifications)
  • PostHog: friend_request_sent, match_invite_sent, match_invite_accepted
  • Versión app → 1.8.0 (app.json, package.json)

También incluido desde develop (post–v1.7.1 en main)

  • Ligas, stats de perfil / ELO / H2H, leaderboard, contacto nativo (Fase 6 / v1.7)
  • Security hardening (lifecycle ligas, ACL, Dependabot / SECURITY.md)
  • CI: permisos mínimos de GITHUB_TOKEN
  • Docs alineadas (REQUIREMENTS.md, TASKS.md)

Checklist previo al merge

  • Aplicar migraciones 108110 (y pendientes de develop) en Supabase producción
  • Publicar Edge Function process-notifications actualizada
  • Regenerar / verificar database.types.ts si hace falta tras aplicar migraciones
  • QA rápida: amigos, invitaciones, prefs de notificación, crear/editar partida con amigos

Notas

Open in Web Open in Cursor 

Summary by CodeRabbit

  • Nuevas funcionalidades
    • Añadidas solicitudes de amistad, búsqueda de usuarios y gestión de amigos desde perfiles.
    • Incorporadas invitaciones a partidas, selección de equipo y enlaces compartibles por WhatsApp.
    • Añadidas preferencias de notificación para amistades e invitaciones.
    • Incorporada generación de calendarios para ligas round-robin.
  • Mejoras
    • Mejoradas estadísticas, seguridad, privacidad y anonimización al eliminar cuentas.
    • Integradas actualizaciones en tiempo real para amistades e invitaciones.
  • Correcciones
    • Mejorada la gestión de resultados pendientes y plazas de partida.
  • Documentación
    • Actualizados requisitos, planificación y versión de la aplicación a 1.8.0.

dependabot Bot and others added 16 commits August 10, 2026 07:50
)

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>
@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Japegomez, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: 5af89f1a-fe83-4611-bec9-68f70062091f

📥 Commits

Reviewing files that changed from the base of the PR and between 218fa5f and 536cb57.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (36)
  • README.md
  • REQUIREMENTS.md
  • TASKS.md
  • package.json
  • src/app/(tabs)/matches/[id].tsx
  • src/app/(tabs)/matches/create.tsx
  • src/app/(tabs)/matches/index.tsx
  • src/components/matches/EditMatchTeamModal.tsx
  • src/components/matches/ShareMatchInviteModal.tsx
  • src/components/profile/FriendsSection.tsx
  • src/hooks/useExploreListsRealtimeSync.test.ts
  • src/hooks/useExploreListsRealtimeSync.ts
  • src/hooks/useFriends.test.ts
  • src/hooks/useMatchInvitations.test.ts
  • src/hooks/useMatchInvitations.ts
  • src/hooks/useMatches.ts
  • src/lib/invalidateExploreCaches.test.ts
  • src/lib/matchQueryKeys.test.ts
  • src/lib/matchQueryKeys.ts
  • src/lib/realtimePending.ts
  • src/lib/sessionBackground.ts
  • src/services/friends.service.ts
  • src/services/matchInvitations.service.ts
  • src/services/results.service.ts
  • src/types/database.types.ts
  • src/utils/leagueFixtures.test.ts
  • src/utils/municipalities.test.ts
  • supabase/functions/process-notifications/index.ts
  • supabase/migrations/20260811150000_106_security_hardening.sql
  • supabase/migrations/20260812120000_111_search_users_by_display_name.sql
  • supabase/migrations/20260812125724_115_review_followups_invites_auth.sql
  • supabase/migrations/20260812130000_112_list_my_match_invitations_exclude_cancelled.sql
  • supabase/migrations/20260812140000_113_void_confirmed_result_on_invitation_reject.sql
  • supabase/migrations/20260812150000_114_review_followups_friends_invites.sql
  • supabase/migrations/20260812170000_116_friend_request_status_and_profile_phone.sql
  • supabase/migrations/20260812180000_117_review_followups_search_status_phone.sql
📝 Walkthrough

Walkthrough

Se 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.

Changes

Amistades e invitaciones

Layer / File(s) Summary
Contratos, persistencia y servicios
supabase/migrations/..., src/services/..., src/hooks/..., src/types/database.types.ts
Se añaden tablas, RPC protegidas, tipos, servicios y hooks para amistades e invitaciones.
Flujos de amistad
src/app/(tabs)/profile/..., src/components/profile/...
Los perfiles permiten buscar usuarios, enviar, aceptar, rechazar, cancelar y eliminar amistades.
Flujos de invitación
src/app/(tabs)/matches/..., src/components/matches/...
Las partidas permiten invitar amigos, responder invitaciones, compartir enlaces y validar resultados pendientes.
Validaciones y saneamiento
supabase/migrations/20260812125724_115_review_followups_invites_auth.sql, supabase/migrations/20260812140000_113_void_confirmed_result_on_invitation_reject.sql, supabase/migrations/20260812150000_114_review_followups_friends_invites.sql, supabase/migrations/20260812170000_116_friend_request_status_and_profile_phone.sql
Se restringen accesos, se validan capacidades, se controlan rechazos y se amplía el saneamiento de cuentas.
Preferencias, analítica y sincronización
src/utils/notificationPrefs.ts, src/lib/analytics.ts, supabase/functions/process-notifications/index.ts, src/hooks/useExploreListsRealtimeSync.ts
Se añaden preferencias y eventos para amistades e invitaciones, además de sincronización realtime y pruebas.

Seguridad, ligas y release

Layer / File(s) Summary
Seguridad y estadísticas
supabase/migrations/20260811032000_104_player_stats_async_recompute_queue.sql, supabase/migrations/20260811150000_106_security_hardening.sql
Se corrigen permisos, cron y acceso al recálculo y consulta de estadísticas.
Fixtures de ligas
src/utils/leagueFixtures.ts, src/utils/leagueFixtures.test.ts
Se añade la generación round-robin con byes, doble ronda y conteo de partidos.
Alcance, releases y versión
README.md, REQUIREMENTS.md, TASKS.md, app.json, package.json, .github/workflows/eas.yml
Se actualizan el alcance documentado, los requisitos, las releases, la versión 1.8.0 y los permisos del trabajo quality.

Estimated code review effort: 5 (Critical) | ~120 minutos

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed El título identifica la versión 1.8.0 y resume dos cambios principales: solicitudes de amistad e invitaciones a partidas.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 3
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch develop
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

Notifique 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_notification de 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 win

Haga idempotente el alta en la publicación de realtime.

ALTER PUBLICATION supabase_realtime ADD TABLE falla si la tabla ya pertenece a la publicación. El resto del archivo usa IF 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 win

Añada accessibilityRole al indicador de amistad aceptada.

El View solo aporta accessibilityLabel. Sin accessible ni accessibilityRole, 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 win

Aísla el estado de carga por solicitud.

respond.isPending y cancel.isPending pertenecen a una mutación compartida por cada RequestsList. Por eso Button muestra el indicador y se deshabilita en todas las filas. Guarda los friendship_id en curso y pasa loading solo a la fila correspondiente. No uses solo respond.variables o cancel.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 win

Define 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 que REQUIREMENTS.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 win

Incluye el fallback web en CA_CONTACT1.

Line 122 exige copiar el teléfono visible en web. CA_CONTACT1 solo 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 win

Añade el identificador de lenguaje al bloque Markdown.

El bloque que comienza en Line 58 no especifica un lenguaje. Usa text para 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 win

Concede contents: read al job quality.

permissions: {} limita el GITHUB_TOKEN efectivo del workflow reutilizable, aunque quality.yml declare contents: read. Esto puede hacer fallar actions/checkout y gitleaks/gitleaks-action. Mantén contents: write deshabilitado.

🤖 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 win

El mensaje de WhatsApp usa el nombre del equipo como título de la partida.

buildInviteShareMessage en src/lib/shareInvite.ts (líneas 12-21) compone «¡Únete a esta partida en jugaMUS!» seguido de input.title. Aquí title recibe teamLabel, 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 title y usa el nombre del equipo en meta.

🐛 Corrección propuesta

Añade el título de la partida a las props de InviteFriendsTab y de EditMatchTeamModal, 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 win

El texto del banner es incorrecto y contiene un error gramatical.

Dos problemas en la línea 1166:

  1. La preposición se duplica. La plantilla ya escribe unirte a , y la rama negativa añade al equipo rival. El resultado es «unirte a al equipo rival».
  2. 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: teamAName y teamBName se 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 win

Limita las columnas de get_profile_with_phone y sincroniza su tipo generado.

list_match_invitations está correctamente tipada: p.display_name y mi.status son NOT NULL, por lo que string es correcto.

Sin embargo, get_profile_with_phone usa SELECT p.* y RETURNS SETOF public.profiles. También devuelve badge_showcase, notify_on_friend_request y notify_on_match_invitation, aunque src/types/database.types.ts no los declara. Usa una lista explícita de columnas y actualiza Returns/SetofOptions para 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 win

No 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 Alert o 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 win

Revoque también las escrituras al rol anon.

Supabase concede privilegios por defecto a anon, authenticated y service_role. La línea 283 solo revoca a authenticated. Hoy la RLS bloquea la escritura porque no existe ninguna política INSERT/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 win

Derive 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 value

La analítica marca envío incluso cuando el RPC acepta la amistad.

send_friend_request acepta automáticamente si la otra parte ya tenía una solicitud pendiente (migración 108, líneas 80-87). En ese caso trackFriendRequestSent registra friend_request_sent aunque 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 win

Añada casos para los caminos de datos nulos.

Las pruebas no cubren dos ramas del servicio: sendFriendRequest con data: null y error: null, que debe lanzar 'No se pudo enviar la solicitud'; y listMyFriends/listMyFriendRequests con data: 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 win

Alinee el límite de longitud del mensaje con la base de datos.

El cliente limita el mensaje a 200 caracteres, pero public.friendships.message es TEXT sin restricción (migración 108, línea 13). Cualquier otro cliente puede almacenar textos arbitrarios que después se muestran en FriendsSection. Añada un CHECK (char_length(message) <= 200) o recorte el valor dentro de send_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 win

Añ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_friend deja invitaciones a partidas pendientes entre los antiguos amigos.

invite_friend_to_match exige una amistad aceptada (líneas 187-194 de supabase/migrations/20260812100001_109_match_invitations.sql). respond_match_invitation no 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 win

Deriva la copia de preferencias de EVENT_FIELDS para 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_OFF y las dos funciones. Este cambio lo demuestra: notify_on_friend_request y notify_on_match_invitation se 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 satisfies de la línea 28 comprueba que los miembros de EVENT_FIELDS sean 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 = true

Also 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 win

Este 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 valen true, por lo que la aserción pasa incluso si buildReminderTimingUpdates recalculase 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 TABLE no es idempotente.

La migración usa CREATE TABLE IF NOT EXISTS y CREATE INDEX IF NOT EXISTS, por lo que se puede volver a ejecutar. ALTER PUBLICATION ... ADD TABLE lanza 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 win

Usa isLoading para mostrar el estado de carga de amigos.

useMyFriends usa React Query v5 con enabled: Boolean(userId). Sin sesión, isPending permanece 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

📥 Commits

Reviewing files that changed from the base of the PR and between 13e4812 and 592aad2.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (34)
  • .github/workflows/eas.yml
  • README.md
  • REQUIREMENTS.md
  • TASKS.md
  • app.json
  • package.json
  • src/app/(tabs)/matches/[id].tsx
  • src/app/(tabs)/matches/create.tsx
  • src/app/(tabs)/matches/index.tsx
  • src/app/(tabs)/profile/[userId].tsx
  • src/app/(tabs)/profile/index.tsx
  • src/components/matches/EditMatchTeamModal.tsx
  • src/components/profile/FriendsSection.tsx
  • src/components/profile/SendFriendRequestModal.tsx
  • src/hooks/useFriends.ts
  • src/hooks/useMatchInvitations.ts
  • src/lib/analytics.test.ts
  • src/lib/analytics.ts
  • src/services/friends.service.test.ts
  • src/services/friends.service.ts
  • src/services/matchInvitations.service.test.ts
  • src/services/matchInvitations.service.ts
  • src/services/profiles.service.ts
  • src/types/database.types.ts
  • src/utils/leagueFixtures.test.ts
  • src/utils/leagueFixtures.ts
  • src/utils/notificationPrefs.test.ts
  • src/utils/notificationPrefs.ts
  • supabase/functions/process-notifications/index.ts
  • supabase/migrations/20260811032000_104_player_stats_async_recompute_queue.sql
  • supabase/migrations/20260811150000_106_security_hardening.sql
  • supabase/migrations/20260812100000_108_friendships.sql
  • supabase/migrations/20260812100001_109_match_invitations.sql
  • supabase/migrations/20260812100002_110_remove_friend_and_invite_notifications.sql

Comment thread src/app/(tabs)/matches/[id].tsx Outdated
Comment thread src/app/(tabs)/matches/create.tsx Outdated
Comment thread src/app/(tabs)/matches/create.tsx Outdated
Comment thread src/app/(tabs)/matches/create.tsx Outdated
Comment on lines +373 to +392
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)
}
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Comment thread src/app/(tabs)/profile/[userId].tsx Outdated
Comment on lines +266 to +272
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
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.

Comment thread supabase/migrations/20260812100001_109_match_invitations.sql
Comment thread supabase/migrations/20260812100001_109_match_invitations.sql
Comment on lines +480 to +481
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 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()))
PY

Repository: 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
PY

Repository: 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
PY

Repository: 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.sql

Repository: 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()) }")
PY

Repository: 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}")
PY

Repository: 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.

Comment on lines +9 to +25
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ 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.ts

Repository: 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>
@Japegomez

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 50 minutes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

Conserva todos los identificadores de partida pendientes.

pendingRef.current.matchId conserva solo el último match_id. Si llegan eventos de match_invitations para 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 de matchId y 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 win

Comprueba la correspondencia uno a uno de la segunda vuelta.

first.find(...) solo verifica que cada fixture de second tenga algún espejo en first. No compara multiplicidades. Si second repite 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 win

El 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 en supabase/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 win

El drawer puede quedar abierto si la animación se interrumpe.

start devuelve finished: false cuando otra animación toma el control del valor o cuando se llama stopAnimation. En ese caso la función retorna antes de setOpen(false), clearDrawerState() y onClosed?.(). 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 win

Corrige el mensaje de has_pending_rival_invites.

mapResultRpcError traduce errores de rechazo del RPC. Cuando record_match_result_direct devuelve has_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 win

Evita el cierre directo con una invitación pendiente del equipo A

record_match_result_direct rechaza cualquier invitación pending o accepted cuyo invitee_id no sea el creador, sin comprobar mi.team. Por tanto, con una invitación A creada y invitesB.length === 0, la rama directa lanza has_pending_rival_invites despué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 win

Manejar el conflicto de unicidad en solicitudes simultáneas

La migración 108 define uniq_friendships_pair sobre el par normalizado. FOR UPDATE no bloquea filas inexistentes. Dos llamadas concurrentes pueden pasar IF NOT FOUND; una inserta y la otra propaga unique_violation. Captura el conflicto, vuelve a leer la fila y aplica already_friends o request_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 win

Elimina el ciclo entre los hooks.

src/hooks/useMatches.ts importa invalidateMatchInvitationQueries, mientras src/hooks/useMatchInvitations.ts importa símbolos desde useMatches. Extrae las query keys y los helpers compartidos a un módulo sin hooks. Las exportaciones actuales son declaraciones function, 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 value

Añada la restricción como NOT VALID y valídela después.

ADD CONSTRAINT ... CHECK fuerza un escaneo de tabla y bloquea las escrituras durante el escaneo. La tabla friendships es 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 win

Renombre la columna status a match_status.

La columna de salida status contiene m.status (estado de la partida), no mi.status (estado de la invitación). La tabla match_invitations también tiene una columna status, 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_status y actualice MyMatchInvitationRow en src/services/matchInvitations.service.ts y src/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 value

Diferencie el estado de carga entre aceptar y rechazar.

busyIds se indexa por friendship_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 value

Esta comprobación es inalcanzable.

Las líneas 312-315 ya retornan cuando isPastResultMode es true y pastResult es nulo. Al llegar a la línea 344, pastResult no 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 value

Simplifica el ternario redundante de team_b_player_1.

Las ramas invitesB.length === 1 y la rama final devuelven la misma expresión teamBPlayer1?.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 value

Elimina los dos efectos de recorte de invitaciones.

inviteCapacityA e inviteCapacityB son constantes literales (líneas 220-221) con valores 1 y 2. Sus valores nunca cambian entre renders, así que ambos useEffect se ejecutan una única vez al montar, cuando invitesA e invitesB están vacíos. Nunca recortan nada.

toggleInvite ya impone el límite (líneas 229 y 237) y onSubmit aplica slice sobre 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 win

Saca las llamadas a setValue fuera de los updaters de estado.

setInvitesA y setInvitesB deben 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 en invitesA o invitesB puede 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 | 🔵 Trivial

Añ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 de public.profiles filtrado por status = '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_trgm y crear un índice GIN sobre lower(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 value

Cancela el temporizador de copied al desmontar.

setTimeout programa setCopied(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 ref y 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 win

Invalida también las invitaciones del partido al responder.

onSuccess invalida las invitaciones del usuario y el dashboard, pero no matchInvitationsQueryKey(matchId). La pantalla de detalle consume esa consulta en src/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 expire QUERY_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

📥 Commits

Reviewing files that changed from the base of the PR and between 592aad2 and 218fa5f.

📒 Files selected for processing (36)
  • .github/workflows/eas.yml
  • README.md
  • REQUIREMENTS.md
  • TASKS.md
  • src/app/(tabs)/matches/[id].tsx
  • src/app/(tabs)/matches/create.tsx
  • src/app/(tabs)/matches/index.tsx
  • src/app/(tabs)/profile/[userId].tsx
  • src/app/(tabs)/profile/index.tsx
  • src/components/matches/EditMatchTeamModal.tsx
  • src/components/matches/ShareMatchInviteModal.tsx
  • src/components/matches/SubmitResultModal.tsx
  • src/components/profile/FriendsSection.tsx
  • src/hooks/useExploreListsRealtimeSync.ts
  • src/hooks/useFriends.ts
  • src/hooks/useMatchInvitations.ts
  • src/hooks/useMatches.ts
  • src/lib/analytics.ts
  • src/lib/invalidateExploreCaches.test.ts
  • src/lib/realtimeRowIds.ts
  • src/services/friends.service.test.ts
  • src/services/friends.service.ts
  • src/services/matchInvitations.service.test.ts
  • src/services/matchInvitations.service.ts
  • src/services/profiles.service.ts
  • src/services/results.service.ts
  • src/types/database.types.ts
  • src/utils/leagueFixtures.test.ts
  • src/utils/notificationPrefs.test.ts
  • src/utils/notificationPrefs.ts
  • supabase/migrations/20260812120000_111_search_users_by_display_name.sql
  • supabase/migrations/20260812125724_115_review_followups_invites_auth.sql
  • supabase/migrations/20260812130000_112_list_my_match_invitations_exclude_cancelled.sql
  • supabase/migrations/20260812140000_113_void_confirmed_result_on_invitation_reject.sql
  • supabase/migrations/20260812150000_114_review_followups_friends_invites.sql
  • supabase/migrations/20260812170000_116_friend_request_status_and_profile_phone.sql

Comment thread src/app/(tabs)/matches/[id].tsx
Comment thread src/app/(tabs)/matches/[id].tsx Outdated
Comment thread src/app/(tabs)/matches/create.tsx Outdated
Comment thread src/components/matches/EditMatchTeamModal.tsx Outdated
Comment thread supabase/migrations/20260812125724_115_review_followups_invites_auth.sql Outdated
Comment thread supabase/migrations/20260812150000_114_review_followups_friends_invites.sql Outdated
Japegomez and others added 4 commits August 12, 2026 15:46
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>
@Japegomez
Japegomez merged commit 115eb0b into main Aug 12, 2026
7 checks passed
Japegomez added a commit that referenced this pull request Aug 13, 2026
Keep develop (v1.8.1 + review follow-ups) where the squash of #158 overlapped.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant