Skip to content

💫 improve carousel transitions#701

Merged
cruzdanilo merged 1 commit intomainfrom
carousel
Jan 30, 2026
Merged

💫 improve carousel transitions#701
cruzdanilo merged 1 commit intomainfrom
carousel

Conversation

@dieguezguille
Copy link
Member

@dieguezguille dieguezguille commented Jan 30, 2026

closes #91


Open with Devin

Summary by CodeRabbit

  • New Features

    • Replaced paging with a smoother carousel and improved transition animations
    • Enhanced pagination: distance-based, animated dots that reflect scroll position and theme colors
    • More reliable scroll tracking and progress animations for uninterrupted autoplay and user interaction
    • Responsive sizing adjustments for consistent visuals across screen sizes and platforms
  • Refactor

    • Internal component restructuring to standardize animations and rendering without changing public APIs

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 30, 2026

🦋 Changeset detected

Latest commit: 6077f60

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gemini-code-assist
Copy link

Summary of Changes

Hello @dieguezguille, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request undertakes a significant refactoring of the application's carousel components, specifically within the authentication and benefits sections. By migrating from a custom Animated.FlatList setup to the react-native-reanimated-carousel library, the changes aim to deliver a more fluid and performant user experience through smoother transitions and more responsive pagination animations.

Highlights

  • Carousel Library Migration: The existing custom Animated.FlatList based carousel implementation in Auth.tsx has been replaced with the react-native-reanimated-carousel library, providing a more robust and feature-rich solution for carousels.
  • Enhanced Carousel Transitions: The visual transitions and animations for carousels have been significantly improved by leveraging the advanced capabilities of react-native-reanimated-carousel, allowing for more precise control over item scaling and opacity based on scroll progress.
  • Refactored Pagination Components: The pagination logic in both the authentication flow (Auth.tsx) and the benefits section (BenefitsSection.tsx) has been updated to integrate seamlessly with the new carousel library. This includes the introduction of scrollOffset and isScrolling shared values for dynamic dot animations and progress bar updates.
  • New PaginationDot Component: A dedicated PaginationDot component has been introduced in BenefitsSection.tsx to handle the animated appearance of individual pagination indicators, enhancing the visual feedback during carousel navigation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Jan 30, 2026

Walkthrough

Replaces FlatList-based carousels with a Carousel component across auth and benefits screens; swaps index/x-driven animations for reanimated SharedValue-driven scrollOffset and distance-based pagination, updates ListItem props and pagination signatures, and adds progress animation control and responsive sizing.

Changes

Cohort / File(s) Summary
Changeset
.changeset/grumpy-candies-return.md
Adds a patch changeset for @exactly/mobile with description "💫 improve carousel transitions".
Auth: Carousel & Progress
src/components/auth/Auth.tsx
Replaces Animated.FlatList with Carousel; introduces responsive item sizing (window/aspect ratio), scrollOffset/isScrolling, progress animation lifecycle (start/reset/cancel), snap/scroll handlers, and related import changes.
Auth: ListItem API/Implementation
src/components/auth/ListItem.tsx
Changes ListItem signature to accept animationValue: SharedValue<number> via new ListItemProperties type; converts to named function and exports memo(ListItem); updates animated styles to use animationValue.
Auth: Pagination API & Rendering
src/components/auth/Pagination.tsx
Reworks pagination to accept scrollOffset, isScrolling, and length instead of x; introduces distance-based width/opacity calculation, new calculateDistance helper, and rendering via layout primitives (XStack).
Benefits: Animated Pagination Dots
src/components/benefits/BenefitsSection.tsx
Replaces static indicators with animated PaginationDot components driven by shared scrollOffset; adds distance-based animations, theme-driven colors, and removes currentIndex state in favor of scroll-driven updates.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Carousel
    participant Reanimated as ReanimatedEngine
    participant Auth
    participant ListItem
    participant Pagination

    User->>Carousel: swipe / gesture
    Carousel->>ReanimatedEngine: emit scrollOffset updates
    ReanimatedEngine->>ListItem: provide animationValue (derived from scrollOffset)
    ReanimatedEngine->>Pagination: update dot distances / progress
    Carousel->>Auth: notify snap/end
    Auth->>ReanimatedEngine: start/reset/cancel progress animation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ✨ app: add benefits section #634: Modifies src/components/benefits/BenefitsSection.tsx; adds or changes benefits pagination/animation—directly related to the animated dots changes.
  • ♻️ misc app improvements #633: Modifies auth carousel/pagination/list-item logic (Auth.tsx, Pagination.tsx, ListItem.tsx); overlaps with this PR's auth UI changes.

Suggested reviewers

  • franm91
  • cruzdanilo
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'improve carousel transitions' directly relates to the main change: replacing Animated.FlatList with a Carousel component and refactoring related animation/progress logic.
Linked Issues check ✅ Passed Changes implement carousel scrolling improvements by replacing FlatList with Carousel component, adding smooth progress animations, and updating pagination—aligning with the linked issue.
Out of Scope Changes check ✅ Passed All changes are directly related to carousel/pagination refactoring. No unrelated modifications detected across Auth.tsx, ListItem.tsx, Pagination.tsx, and BenefitsSection.tsx.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch carousel

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 and usage tips.

Copy link

@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 is a great improvement, refactoring the custom carousel implementation to use react-native-reanimated-carousel. This simplifies the code significantly by removing complex manual animation and scrolling logic, and leverages a specialized library for the task. The changes in Auth.tsx, ListItem.tsx, and Pagination.tsx are well-executed. The same pattern has been applied to the benefits carousel, which is great for consistency. I've found a couple of opportunities for improvement regarding code duplication, which I've detailed in my comments.

Comment on lines 18 to 38
function ListItem({ item, animationValue }: ListItemProperties) {
const aspectRatio = useAspectRatio();
const { width, height } = useWindowDimensions();
const itemWidth = Platform.OS === "web" ? height * aspectRatio : width;

// animationValue: -1 (left), 0 (center/active), 1 (right)
/* istanbul ignore next */
const rBackgroundStyle = useAnimatedStyle(() => {
const animatedScale = interpolate(
x.value,
[(index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth],
[0, 1, 0],
Extrapolation.CLAMP,
);
const interpolatedOpacity = interpolate(
x.value,
[(index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth],
[0, 1, 0],
Extrapolation.CLAMP,
);
const animatedScale = interpolate(animationValue.value, [-1, 0, 1], [0.5, 1, 0.5], Extrapolation.CLAMP);
const interpolatedOpacity = interpolate(animationValue.value, [-1, 0, 1], [0.3, 1, 0.3], Extrapolation.CLAMP);
return { transform: [{ scale: animatedScale }], opacity: interpolatedOpacity };
}, [index, x]);
}, [animationValue]);

/* istanbul ignore next */
const rImageStyle = useAnimatedStyle(() => {
const animatedScale = interpolate(
x.value,
[(index - 1) * itemWidth, index * itemWidth, (index + 1) * itemWidth],
[0.5, 1, 0.5],
Extrapolation.CLAMP,
);
const animatedScale = interpolate(animationValue.value, [-1, 0, 1], [0.7, 1, 0.7], Extrapolation.CLAMP);
return { transform: [{ scale: animatedScale }] };
}, [index, x]);
}, [animationValue]);

return (
<View width={itemWidth} aspectRatio={aspectRatio} justifyContent="center" alignItems="center">

Choose a reason for hiding this comment

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

medium

The itemWidth is calculated here and also in the parent Auth component. This duplication can lead to inconsistencies. In fact, the calculation in Auth.tsx includes Math.max(..., 250) which is missing here.

Since the Carousel component already sets the width for each item's container, this component should just fill that container. You can remove the itemWidth calculation and set width="100%" on the root View.

function ListItem({ item, animationValue }: ListItemProperties) {
  const aspectRatio = useAspectRatio();

  // animationValue: -1 (left), 0 (center/active), 1 (right)
  /* istanbul ignore next */
  const rBackgroundStyle = useAnimatedStyle(() => {
    const animatedScale = interpolate(animationValue.value, [-1, 0, 1], [0.5, 1, 0.5], Extrapolation.CLAMP);
    const interpolatedOpacity = interpolate(animationValue.value, [-1, 0, 1], [0.3, 1, 0.3], Extrapolation.CLAMP);
    return { transform: [{ scale: animatedScale }], opacity: interpolatedOpacity };
  }, [animationValue]);

  /* istanbul ignore next */
  const rImageStyle = useAnimatedStyle(() => {
    const animatedScale = interpolate(animationValue.value, [-1, 0, 1], [0.7, 1, 0.7], Extrapolation.CLAMP);
    return { transform: [{ scale: animatedScale }] };
  }, [animationValue]);

  return (
    <View width="100%" aspectRatio={aspectRatio} justifyContent="center" alignItems="center">

Comment on lines +66 to +74
function calculateDistance(scrollOffset: number, index: number, length: number) {
"worklet";
const normalizedOffset = ((scrollOffset % length) + length) % length;
let distance = Math.abs(normalizedOffset - index);
if (distance > length / 2) {
distance = length - distance;
}
return distance;
}

Choose a reason for hiding this comment

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

medium

This calculateDistance worklet is identical to the one in src/components/auth/Pagination.tsx. To avoid code duplication and improve maintainability, you should extract this function into a shared utility file (e.g., in src/utils/animation.ts) and import it in both places.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional flags.

Open in Devin Review

@sentry
Copy link

sentry bot commented Jan 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.64%. Comparing base (021a7bd) to head (6077f60).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #701      +/-   ##
==========================================
+ Coverage   64.54%   64.64%   +0.10%     
==========================================
  Files         173      173              
  Lines        5378     5385       +7     
  Branches     1509     1505       -4     
==========================================
+ Hits         3471     3481      +10     
+ Misses       1727     1723       -4     
- Partials      180      181       +1     
Flag Coverage Δ
e2e 43.12% <100.00%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/benefits/BenefitsSection.tsx (1)

153-160: ⚠️ Potential issue | 🟠 Major

Avoid accidental taps in the carousel by using TapGestureHandler.

BenefitCard is triggered via onPress inside the carousel; ensure the card uses TapGestureHandler (or wrap it here) so swipe gestures don't fire taps.

#!/bin/bash
# Verify BenefitCard uses TapGestureHandler instead of plain onPress
rg -n "TapGestureHandler|onPress" -g 'BenefitCard.tsx' -C2

Based on learnings, "In benefit card components (e.g., src/components/benefits/BenefitCard.tsx) that use a carousel (react-native-reanimated-carousel) with Tamagui, prefer using TapGestureHandler from react-native-gesture-handler instead of Tamagui's built-in onPress for card items."

🤖 Fix all issues with AI agents
In `@src/components/auth/Pagination.tsx`:
- Around line 10-19: The duplicate circular-scroll logic in calculateDistance
should be extracted to a shared helper: create a single exported function (e.g.,
calculateDistance) in a new utility module and replace the inline
implementations in both Pagination component and BenefitsSection by importing
that helper; update the components to call the shared calculateDistance
(preserving the same signature scrollOffset, index, length) and remove the
duplicated function bodies to avoid drift, and update any relevant
imports/exports or tests that referenced the old local implementations.
- Around line 85-96: The row wrapper uses React Native's View but per guidelines
should use Tamagui's XStack; replace the outer <View flexDirection="row" ...>
with <XStack ...> while preserving props (alignItems/justifyContent/gap) and
passing the same children into PaginationComponent (key, index, length,
scrollOffset, progress, isScrolling, activeColor using
theme.interactiveBaseBrandDefault.val); ensure you import XStack at top and
remove the now-unnecessary View import.

Comment on lines +10 to +19
/* istanbul ignore next */
function calculateDistance(scrollOffset: number, index: number, length: number) {
"worklet";
const normalizedOffset = ((scrollOffset % length) + length) % length;
let distance = Math.abs(normalizedOffset - index);
if (distance > length / 2) {
distance = length - distance;
}
return distance;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Extract calculateDistance into a shared helper to avoid drift.

The same logic exists in src/components/benefits/BenefitsSection.tsx; a shared utility reduces divergence.

🤖 Prompt for AI Agents
In `@src/components/auth/Pagination.tsx` around lines 10 - 19, The duplicate
circular-scroll logic in calculateDistance should be extracted to a shared
helper: create a single exported function (e.g., calculateDistance) in a new
utility module and replace the inline implementations in both Pagination
component and BenefitsSection by importing that helper; update the components to
call the shared calculateDistance (preserving the same signature scrollOffset,
index, length) and remove the duplicated function bodies to avoid drift, and
update any relevant imports/exports or tests that referenced the old local
implementations.

@dieguezguille dieguezguille force-pushed the carousel branch 2 times, most recently from 6ddca82 to dfece93 Compare January 30, 2026 18:24
Comment on lines -100 to -102
const timer = setInterval(() => {
/* istanbul ignore next */
progress.value = withTiming(progress.value + 0.2, { duration: 1000, easing: Easing.linear }, () => {

This comment was marked as outdated.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/components/auth/ListItem.tsx`:
- Line 6: The component uses Tamagui's View as a pure layout wrapper—replace the
import and usages with YStack: change the import from "View" to "YStack" and
replace each <View ...> wrapper (including the occurrences around lines 32-39)
with <YStack ...>, preserving the same layout props but using YStack's
primitives and closing tags (update any View-specific props if needed to Tamagui
stack equivalents); ensure the component export/function ListItem remains
unchanged.

import { Extrapolation, interpolate, useAnimatedStyle } from "react-native-reanimated";

import { useWindowDimensions, View } from "tamagui";
import { View } from "tamagui";
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/auth/ListItem.tsx

Repository: exactly/exa

Length of output: 2007


Replace View with YStack for layout.
The wrapper is a pure layout container applying layout properties and should use Tamagui's stack primitives.

♻️ Suggested refactor
-import { View } from "tamagui";
+import { YStack } from "tamagui";
@@
-    <View width="100%" height="100%" justifyContent="center" alignItems="center">
+    <YStack width="100%" height="100%" justifyContent="center" alignItems="center">
@@
-    </View>
+    </YStack>

Also applies to: lines 32-39

🤖 Prompt for AI Agents
In `@src/components/auth/ListItem.tsx` at line 6, The component uses Tamagui's
View as a pure layout wrapper—replace the import and usages with YStack: change
the import from "View" to "YStack" and replace each <View ...> wrapper
(including the occurrences around lines 32-39) with <YStack ...>, preserving the
same layout props but using YStack's primitives and closing tags (update any
View-specific props if needed to Tamagui stack equivalents); ensure the
component export/function ListItem remains unchanged.

@cruzdanilo cruzdanilo merged commit 6077f60 into main Jan 30, 2026
15 checks passed
@cruzdanilo cruzdanilo deleted the carousel branch January 30, 2026 19:56
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.

ui: make carousel scrolling continuous

2 participants

Comments