Skip to content

feat(squads): social streak groups, Kudos, leaderboard (#220)#223

Open
Eliaaazzz wants to merge 2 commits into
mainfrom
feat/squads
Open

feat(squads): social streak groups, Kudos, leaderboard (#220)#223
Eliaaazzz wants to merge 2 commits into
mainfrom
feat/squads

Conversation

@Eliaaazzz
Copy link
Copy Markdown
Owner

Summary

Implements Feature 1 of 3 from issue #220 — Squads. Adapts two of the strongest published retention mechanics in consumer fitness (Strava Clubs + Duolingo group streak) to the nutrition-logging lane.

Why this: Strava Club members are 3.5× more likely to remain active after 12 months; apps with social streaks see avg streak length 5.69d vs 4.25d. Source: StriveCloud / Trophy Strava case study.

Closes #220.

What's in this PR

Backend (Spring Boot, Java 21):

  • V52__create_squads.sqlsquads, squad_members (composite PK), meal_log_kudos
  • 8 new ErrorCode entries (2010–2017) plumbed through GlobalExceptionHandler via a new SquadException
  • SquadService — create / join / leave / list / detail / removeMember, owner-transfer on leave, dissolve on last-member-leave, shared-streak evaluation, 7-day leaderboard with "Warming Up" tier (members with <3 distinct logged days are unranked)
  • KudosService — toggle with self-forbidden, 7-day window, and cross-squad-membership checks
  • SquadStreakScheduler@EnableScheduling + hourly cron; per-squad timezone idempotent evaluation
  • InviteCodeGenerator — 6-char codes from a 32-char unambiguous alphabet (no 0/O/1/I); collision-checked
  • 18 Mockito unit tests covering SquadServiceTest, KudosServiceTest, InviteCodeGeneratorTest

Frontend (React Native + Expo, TypeScript):

  • SquadsScreen — list with pull-to-refresh, Create / Join with code CTAs, empty state
  • SquadDetailScreen — streak hero header, share-via-system invite, 7-day leaderboard (auto-refreshes every 5min while focused), members list, leave with confirm
  • SquadCreateModal — 16-emoji palette + 30-char name input
  • JoinSquadModal — Apple-style 6-cell segmented code entry, regex-validated to the same 32-char alphabet
  • KudosButton — optimistic UI, in-flight debounce against rapid double-tap, revert on API error, haptics
  • SquadCard, SquadStreakHeader (reuses personal StreakBadge tier system)
  • 4 Jest + RTL tests for KudosButton

Acceptance Criteria coverage

AC Status Notes
AC-1 Squad lifecycle Create / join / 6-code / 10-member cap / 3-squad-per-user cap / owner-transfer on leave / dissolve on last leave
AC-2 Kudos ✅ (API + button) Toggle is idempotent server-side; self-/expiry-/cross-squad validation; UI button ready to drop into MealLogCard
AC-3 Squad streak Hourly scheduler evaluates each squad in its own timezone; idempotent on lastActiveDay
AC-4 Leaderboard 7-day, Warming-Up tier guards cold-start; auto-refreshes every 5min
AC-5 Privacy & non-functional All endpoints reject non-members (SQUAD_ACCESS_DENIED); screens wrapped with withErrorBoundary; no hardcoded hex (theme tokens only)

What's intentionally out of scope (follow-ups)

  • MealLogCard integration of KudosButton — component is built and unit-tested; wiring it into the existing meal-log card is a small UI patch, kept separate to keep this PR reviewable.
  • Profile-screen entry point — routes are registered (navigation.navigate('Squads') works); a one-line link from ProfileScreen will follow.
  • Detox/Playwright e2e flows — unit tests land here; e2e suite is in a follow-up to avoid mixing test infra changes.
  • Push notification on streak milestone — uses existing notification service; deferred to feature 3 (Challenges).
  • Feature flag (feature.squads.enabled) — to be added when rolling out; not gated in this PR since it's behind nav routes only reachable via explicit nav.

Design philosophy compliance (per CLAUDE.md)

  • ✅ Glass morphism via BentoCard for all cards; no flat opaque cards.
  • ✅ All colors via BRAND_COLORS tokens — no hardcoded hex.
  • ✅ Reanimated spring animations on KudosButton (damping 12–14, stiffness 150–220).
  • ✅ Every new screen wrapped with withErrorBoundary.
  • ✅ Friction budget: log-a-meal is unchanged; Kudos = 1 tap; create-a-squad ≤3 screens.

Test plan

  • SquadServiceTest covers create / join (full / dup / invalid-code / limit), leave (last-member dissolve, non-member reject), streak (increment / reset preserves longest / idempotent), leaderboard (Warming Up trailing, non-member reject), shareSquad (overlap / disjoint).
  • KudosServiceTest covers toggle add / toggle remove / self-forbidden / expired-window / non-shared-squad / unknown-meal.
  • InviteCodeGeneratorTest covers shape (32-char alphabet, no confusables), retry on collision, give-up after MAX_ATTEMPTS.
  • KudosButton.test.tsx covers optimistic update + reconcile, revert on error, in-flight debounce, disabled-state suppression.
  • tsc --noEmit passes clean for all new/modified frontend files.
  • Manual: run ./gradlew test locally and verify Flyway applies V52 against a fresh Postgres.
  • Manual: open app, create a squad, copy code, join from another account, verify both members appear and leaderboard renders.

🤖 Generated with Claude Code

…220)

Implements feature 1 of 3 from issue #220 — adapts the Strava Clubs +
Duolingo group-streak retention pattern to nutrition logging.

Backend (Spring Boot, Java 21):
- V52 migration: squads, squad_members (composite PK), meal_log_kudos
- 8 new ErrorCodes (2010–2017) wired into GlobalExceptionHandler
- SquadService: create / join / leave / list / detail / removeMember,
  shared-streak evaluation, 7-day leaderboard with Warming Up tier
- KudosService: toggle with self-/expiry-/cross-squad validation
- SquadStreakScheduler: hourly per-squad-timezone evaluation, idempotent
- InviteCodeGenerator: 6-char codes from a 32-char unambiguous alphabet
- 18 Mockito unit tests covering AC-1..AC-5

Frontend (React Native + Expo):
- Squads list screen with Create / Join CTAs and pull-to-refresh
- Squad detail screen with streak header, share-via-system, leaderboard
- KudosButton component (optimistic UI, in-flight debounce, revert on error)
- SquadCreateModal (emoji palette + name) and JoinSquadModal (segmented code)
- 4 Jest tests for KudosButton (toggle, revert, debounce, disabled)

Routes registered in AppNavigator; entry-point wiring from Profile is a
small follow-up. No design-system tokens were introduced — all theming
goes through BRAND_COLORS / spacing per CLAUDE.md.

Closes #220

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 6, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
aurafit 51bff4a May 07 2026, 07:09 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 6, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
aurafitness 51bff4a May 07 2026, 07:10 AM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 6, 2026

Deploying aurafitness-2 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 51bff4a
Status: ✅  Deploy successful!
Preview URL: https://f043682a.aurafitness-2.pages.dev
Branch Preview URL: https://feat-squads.aurafitness-2.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 6, 2026

Deploying aurafitness with  Cloudflare Pages  Cloudflare Pages

Latest commit: 51bff4a
Status: ✅  Deploy successful!
Preview URL: https://b5bdb585.aurafitness.pages.dev
Branch Preview URL: https://feat-squads.aurafitness.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 6, 2026

Deploying aurafitness-1 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 51bff4a
Status: ✅  Deploy successful!
Preview URL: https://ae175f66.aurafitness-1.pages.dev
Branch Preview URL: https://feat-squads.aurafitness-1.pages.dev

View logs

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new 'Squads' feature, enabling users to form groups, track shared streaks, and interact via kudos on meal logs. The implementation includes comprehensive backend support with new entities, services for squad management and streak evaluation, database migrations, and a robust test suite. On the frontend, the PR adds necessary UI components and screens for squad creation, joining, and detailed viewing. A review comment suggests adding a dedicated test case for the ownership transfer logic in the leave method of SquadService to ensure business logic robustness.

Comment on lines +182 to +187

assertThatThrownBy(() -> service.leave(userB, squadId))
.isInstanceOf(SquadException.class)
.extracting(e -> ((SquadException) e).getErrorCode())
.isEqualTo(ErrorCode.SQUAD_ACCESS_DENIED);
}
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.

medium

The leave method in SquadService includes logic for transferring ownership to the earliest joiner if the current owner leaves and other members remain. This is a critical piece of business logic that should have a dedicated test case to ensure its correctness. Adding a test here would improve the robustness of the test suite.

Addresses gemini-code-assist's review on PR #223: SquadService.leave()
contains the only place where ownership transfers to the earliest joiner
when the current owner leaves with members remaining. That branch was
previously uncovered.

Adds two tests in SquadServiceTest:

- leave_ownerWithRemainingMembers_transfersOwnershipToEarliestJoiner —
  three members; owner leaves; the earliest-joinedAt member is promoted
  (verified against a deliberately out-of-order findAllBySquadId list so
  the test catches a regression where ordering is taken from the list
  instead of from joinedAt). Verifies the new owner is persisted, the
  promoted member's role flips to "owner", and the squad is not
  dissolved.

- leave_nonOwnerWithRemainingMembers_doesNotTransferOwnership —
  guard test: when a non-owner leaves, no ownership transfer occurs and
  squadRepository.save() is never called.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Eliaaazzz
Copy link
Copy Markdown
Owner Author

Addressed in 51bff4a.

Added two tests to SquadServiceTest covering the owner-transfer branch in SquadService.leave():

Test What it verifies
leave_ownerWithRemainingMembers_transfersOwnershipToEarliestJoiner When the owner leaves with members remaining, the earliest-joinedAt member is promoted. The mock returns findAllBySquadId in deliberately out-of-order list order so the test catches a regression where promotion is keyed off list position instead of joinedAt. Verifies new ownerUserId, role flip to "owner", squad NOT dissolved, and both squadMemberRepository.save(promoted) + squadRepository.save(squad) are called.
leave_nonOwnerWithRemainingMembers_doesNotTransferOwnership Guard test: a non-owner leaving must not trigger any ownership-transfer side effects (squadRepository.save is never called, ownerUserId unchanged).

Local test run: SquadServiceTest 18/18 pass (was 16/16).

Thanks for the review!

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.

feat: Squads — social streak groups, Kudos, weekly leaderboard (Strava Clubs pattern)

1 participant