diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
index a288eb67..7210fe76 100644
--- a/.github/workflows/preview.yml
+++ b/.github/workflows/preview.yml
@@ -2,6 +2,7 @@ name: Create EAS Preview
on:
pull_request_target:
+ workflow_dispatch:
permissions:
contents: read
diff --git a/frontend/A Design Blueprint for a Modern, Minimalist Expens.md b/frontend/A Design Blueprint for a Modern, Minimalist Expens.md
new file mode 100644
index 00000000..6ec9b52e
--- /dev/null
+++ b/frontend/A Design Blueprint for a Modern, Minimalist Expens.md
@@ -0,0 +1,339 @@
+# A Design Blueprint for a Modern, Minimalist Expense-Splitting Application for Gen Z
+
+## **Section 1: A Vision for the Modern "Splitwiser"**
+
+### **1.1. Introduction: Beyond Basic Bill Splitting**
+
+This document presents a comprehensive design blueprint for the strategic repositioning of an expense-splitting application, codenamed "Splitwiser," to capture the Generation Z (Gen Z) market. The objective extends beyond a mere user interface (UI) refresh; it is a fundamental reimagining of the user experience (UX) to align with the distinct digital behaviors, aesthetic preferences, and core values of this demographic. The guiding design philosophy is **"Expressive Minimalism."** This principle seeks a deliberate and harmonious balance between two seemingly opposing forces: the Gen Z demand for authenticity, personalization, and vibrant self-expression , and the functional clarity and efficiency inherent in a minimalist aesthetic.
+
+This philosophy is further grounded in the non-negotiable requirements of a modern financial technology (fintech) product. Every design decision must reinforce user trust, communicate security, and present financial data with absolute, unambiguous clarity. The resulting application will not only be visually appealing but also fast, intuitive, and fundamentally trustworthy—a tool that feels less like a sterile ledger and more like an integrated, seamless part of a modern social life.
+
+### **1.2. The Core Problem: The "90s Feel" and the Gen Z Disconnect**
+
+The assessment that the current application possesses a "90s feel" points to a critical disconnect with its target audience. Gen Z, as true digital natives, have baseline expectations for digital products that were not yet established in earlier eras of web and application design. A "90s feel" typically implies several UX and UI deficiencies that are particularly alienating to this user group:
+
+- **Visual Clutter and Low Information Density:** Older interfaces often feature excessive borders, gradients, drop shadows, and a lack of strategic whitespace, leading to high cognitive load and a cluttered appearance that Gen Z finds inefficient and unappealing.
+- **Perceived and Actual Slowness:** The visual language of the 90s was not optimized for the perception of speed. Modern design prioritizes minimalism in part because fewer on-screen elements contribute directly to faster load times and a more responsive feel—a critical factor for a generation with an average attention span of eight seconds.
+- **Outdated Interaction Patterns:** The design likely lacks modern, gesture-driven navigation, satisfying microinteractions, and the seamless, fluid transitions that are now standard in top-tier mobile applications.
+- **Lack of Authenticity and Personalization:** A generic, one-size-fits-all interface feels impersonal and corporate, failing to provide the options for customization and self-expression that Gen Z actively seeks in their digital tools.
+
+This combination of factors results in an application that feels cumbersome, untrustworthy, and fundamentally out of touch with the expectations of its intended users, creating a significant barrier to adoption and sustained engagement.
+
+### **1.3. The Solution: A Blueprint for a Gen Z-Centric Fintech Experience**
+
+This report provides a complete, actionable blueprint to bridge that gap. It is structured to guide development from foundational principles to pixel-perfect implementation. The journey begins with a deep analysis of the target user, translating their psychological and behavioral traits into concrete design principles. From there, it establishes a comprehensive design system—the single source of truth for the app's visual language, encompassing color, typography, and layout.
+
+Subsequently, the blueprint details the design of core, reusable components and applies this system to reimagine the application's key screens and user flows. Finally, it specifies the use of motion and animation to create a polished, modern, and delightful experience. Every recommendation is deliberate, justified by established UX research and fintech best practices, and aimed at creating an application that is not just used, but loved.
+
+## **Section 2: Deconstructing the Gen Z Mindset: Core Principles for Digital Engagement**
+
+To design an application that resonates with Gen Z, one must first understand the core tenets that govern their digital interactions. This is a generation that grew up with smartphones as an extension of themselves, shaping a unique set of expectations and behaviors. The following principles translate these characteristics into an actionable design framework.
+
+### **2.1. The 8-Second Rule: Designing for Immediacy**
+
+The most cited characteristic of Gen Z is a digitally conditioned attention span of approximately eight seconds. This is not a measure of intelligence, but a reflection of an environment saturated with information, where users have become highly efficient at filtering out irrelevant or slow experiences. For a mobile application, this has profound implications.
+
+Performance is not a final optimization step; it is a core, foundational feature. A significant percentage of users, 79% according to one study, will abandon a site or app that is slow to load or respond. The design must be architected for speed from the very beginning. This imperative directly informs the choice of a minimalist aesthetic. A clean, uncluttered interface with fewer elements and optimized assets inherently leads to faster load times and a smoother user experience. This makes minimalism a functional requirement, not merely a stylistic choice.
+
+This principle of immediacy translates into several key design directives:
+
+- **Minimalist Layouts:** Employ generous whitespace, a clear visual hierarchy, and a reduction of all non-essential elements. This minimizes cognitive load, allowing users to process information and make decisions faster, combating the decision fatigue that cluttered interfaces can induce.
+- **Scannable Content:** Structure all information for quick scanning. Use bold headlines, short paragraphs, and bullet points. Users, particularly Gen Z, prefer to absorb information visually and quickly rather than reading long blocks of text.
+- **Instant Feedback:** Every user action must have an immediate and perceptible reaction. This is achieved through microinteractions—small, targeted animations that confirm an action, such as a button press or a successful data entry. This reassures the user that the system is responsive and working correctly.
+
+### **2.2. Authenticity Over Polish: Building Trust Through Transparency**
+
+Gen Z possesses a highly developed sensitivity to inauthenticity. They are skeptical of overly polished, corporate branding and can easily detect when a product feels contrived or insincere. Building trust with this demographic requires a commitment to transparency that is reflected directly in the UI.
+
+The app's visual language must feel genuine and relatable. This means a strict "no fake fronts" policy. Generic stock photos of perfectly happy, homogenous groups should be avoided. Instead, the design should leverage high-quality, diverse illustrations or user-generated content (if applicable) that reflect the real world of the user base. As seen with brands like ASOS, which uses unretouched imagery, this raw and honest approach builds significant credibility.
+
+For a fintech application like Splitwiser, this principle of transparency is paramount and must be applied to its core functionality:
+
+- **Clarity in Financial Data:** The interface must present all financial information—who owes whom, how splits are calculated, and total balances—with absolute clarity. There should be no ambiguity, hidden fees, or complex jargon.
+- **Upfront Policies:** Any information regarding data usage or privacy must be presented clearly and accessibly, not buried in fine print. This proactive approach to transparency builds foundational trust.
+- **Honest Error States:** When something goes wrong, the app should communicate it clearly and honestly, providing helpful guidance on how to resolve the issue rather than displaying a generic or confusing error code.
+
+### **2.3. Personalization as Identity: The Customizable Interface**
+
+Gen Z views their digital tools and spaces as extensions of their personal identity. They expect experiences that can be tailored and customized to reflect their individuality. A one-size-fits-all approach feels dated and disconnected.
+
+The application design must incorporate meaningful customization options. This can start with foundational features that are now baseline expectations for this demographic, such as a well-designed Dark Mode. Dark Mode is not just an aesthetic preference; it offers tangible benefits like reduced eye strain and improved battery life on OLED screens, which is highly valued by heavy screen users.
+
+Beyond this, the app can offer more expressive forms of personalization, drawing inspiration from platforms like Revolut and Discord, which allow users to change themes and customize the UI. For Splitwiser, this could manifest as:
+
+- **Accent Color Customization:** Allowing users to select a personal accent color that is applied to key UI elements like buttons and active states.
+- **Customizable Group Icons/Banners:** Giving users the ability to personalize their shared expense groups with photos or unique icons.
+- **Dynamic Interfaces:** The app can feel more personal by dynamically adapting to user behavior. For instance, the dashboard could highlight the group or person the user interacts with most frequently, mirroring the dynamic, personalized nature of platforms like Spotify.
+
+### **2.4. Values-Driven Design: Ethics, Inclusivity, and Sustainability**
+
+Gen Z is more likely to engage with and remain loyal to brands that align with their social and ethical values. Design is a powerful medium for communicating these values. For this generation, inclusivity and accessibility are not optional add-ons; they are fundamental requirements for a good product.
+
+A failure to design inclusively is a significant misstep that can lead to immediate user abandonment. The design process must therefore embed these principles from the outset:
+
+- **Accessibility as a Baseline:** The application must adhere to Web Content Accessibility Guidelines (WCAG) standards. This includes ensuring a minimum text-to-background contrast ratio of 4.5:1, using scalable typography that respects the user's system-level font size settings, and providing clear, descriptive labels for all interactive elements.
+- **Inclusive Representation:** The visual elements of the app, such as illustrations or default avatars, must be representative of a diverse range of identities, backgrounds, and abilities.
+- **Gender-Neutral Language:** The copy used throughout the application should be inclusive and avoid gendered assumptions, using neutral terms wherever possible.
+
+By building these values directly into the product's design, the application demonstrates a genuine commitment to its users, fostering a deeper sense of trust and community.
+
+## **Section 3: The Foundation: A Minimalist, Glassmorphic Design System**
+
+A design system is the single source of truth that groups all the elements that will allow the teams to design, realize, and develop a product. For this Splitwiser redesign, the system will codify the "Expressive Minimalism" philosophy into a set of reusable components and clear standards. This ensures consistency, accelerates development, and provides a robust framework for future growth. The aesthetic core of this system will be **Strategic Glassmorphism**.
+
+### **3.1. The Aesthetic Core: Strategic Glassmorphism**
+
+Glassmorphism is a UI trend characterized by a frosted-glass effect, where a semi-transparent, blurred layer sits on top of a colorful background, creating a sense of depth and hierarchy. This modern, sleek aesthetic is visually appealing and aligns with the desired futuristic feel. However, its implementation must be strategic to avoid common pitfalls, particularly around accessibility.
+
+The effect will not be used for all elements. Its inherent low-contrast properties can make text and interactive elements difficult to read, especially for users with visual impairments. Therefore, its application will be carefully constrained. Glassmorphism will be used as a tool to establish hierarchy and add visual interest, not as a universal decorative style.
+
+- **Application:** Reserved for non-critical, background-level surfaces such as the main content cards on the dashboard, modal overlays, and widget backgrounds.
+- **Exclusion:** It will *not* be used for primary interactive elements like buttons, input fields, or navigation bars. All critical text and controls will be placed on fully opaque surfaces to guarantee high contrast and legibility, in adherence with WCAG standards.
+- **Properties:** The effect will be defined by a consistent set of properties:
+- **Transparency:** A background color with an alpha channel, typically set to an opacity between 10% and 40%, depending on the background.
+- **Background Blur:** A significant blur radius (e.g., 20px to 40px) applied to the content behind the element.
+- **Subtle Border:** A 1px, semi-transparent light border to define the edge of the glass and enhance the illusion of a physical object floating in space.
+
+This targeted approach allows the app to leverage the modern appeal of Glassmorphism while maintaining the robust accessibility and clarity required of a fintech product.
+
+### **3.2. Color Palette: Vibrant Trust and Digital Chic**
+
+The color palette is engineered to evoke both the trust required for a financial application and the vibrant energy that appeals to Gen Z. It is divided into four distinct categories, each with a specific purpose.
+
+- **Primary Palette (Fintech Trust):** This forms the foundation of the UI, used for backgrounds, primary text, and structural elements. It is built on deep, stable colors that subconsciously communicate security and professionalism.
+- **Deep Blue:** Associated with stability, trust, and security.
+- **Dark Green:** Connotes wealth, growth, and prosperity.
+- **Accent Palette (Gen Z Expression):** These are bold, high-energy colors used sparingly to draw attention to key actions and add personality. They are reserved for primary calls-to-action (CTAs), notifications, progress indicators, and user-selectable theme highlights. This strategy aligns with the trend of using bright, optimistic primary colors to appeal to a younger demographic.
+- **Vibrant Purple:** A popular choice in fintech, suggesting innovation and creativity.
+- **Electric Blue/Aqua:** A modern, digital-native hue that feels energetic and fresh.
+- **Viva Magenta:** A bold, expressive color that stands out and conveys confidence.
+- **Neutral Palette (Minimalist Foundation):** A comprehensive grayscale range is essential for creating clean layouts and supporting both light and dark modes. This palette provides the necessary contrast for text and ensures content remains the primary focus.
+- **Semantic Palette:** A standardized set of colors for communicating system status is crucial for user feedback and error prevention.
+- **Success:** A clear, accessible green.
+- **Warning:** An amber or yellow.
+- **Error/Destructive:** A distinct, understandable red.
+
+### **3.3. Typography: Expressive Clarity**
+
+Typography is a primary tool for establishing information hierarchy, conveying brand personality, and ensuring readability. The system will be built around a single, versatile typeface to maintain minimalist consistency.
+
+- **Primary Typeface: Inter** This sans-serif typeface is the ideal choice for several reasons. It was designed by Rasmus Andersson specifically for high legibility on computer screens, featuring a tall x-height that makes lowercase text easy to read even at small sizes. As a variable font, it offers a wide range of weights, allowing for expressive, bold headlines that appeal to Gen Z without sacrificing the clarity needed for dense financial data. Its widespread adoption in modern UI design (it is the default in our design system at Shipfaster UI) provides a sense of familiarity and professionalism.
+- **Type Scale** A clear and consistent type scale is essential for creating a logical visual hierarchy. The scale will be defined with styles for different semantic purposes, adhering to best practices from Apple's Human Interface Guidelines and Google's Material Design. Light font weights (Ultralight, Thin, Light) will be avoided as they compromise legibility, especially on mobile screens.
+
+### **3.4. Iconography: Simple and Functional**
+
+Icons serve as a universal language, reducing cognitive load by communicating concepts visually. The icon style must align with the overall minimalist aesthetic: clean, simple, and instantly recognizable.
+
+- **Library:** A high-quality, open-source icon library such as **Feather Icons** or **Heroicons** is recommended. These libraries offer a comprehensive set of icons with a consistent, modern, and lightweight stroke-based style.
+- **Style:** Icons should primarily be in an outlined style for inactive or default states. The active state (e.g., the current tab in the navigation bar) should be represented by a filled version of the same icon. This is a common and highly intuitive pattern that provides clear visual feedback to the user.
+- **Consistency:** All icons used throughout the application must come from the same family to ensure visual harmony and a professional feel.
+
+### **3.5. Layout, Spacing, and Grids**
+
+A disciplined approach to spacing is the invisible scaffolding that holds a minimalist design together. It creates rhythm, reduces clutter, and guides the user's eye through the content.
+
+- **Spacing Unit:** The system will be built on a base unit of **8px**. All margins, padding, and component dimensions will be defined as multiples of this unit (e.g., 8px, 16px, 24px, 32px). This ensures mathematical consistency and visual harmony across the entire interface.
+- **Layout:** For most screens, especially those containing forms or feeds of information, a **single-column layout** will be enforced. This is a mobile-first best practice that creates a clear, linear path for the user to follow, eliminating ambiguity and improving comprehension on narrow screens.
+- **Whitespace:** Generous and strategic use of negative space is a core tenet of minimalism. It will be used to separate distinct content areas without relying on excessive lines or dividers, allowing the content to "breathe" and making complex financial information more digestible.
+
+The following table provides the specific design tokens that form the foundation of this system. These tokens should be defined as variables in the application's codebase to ensure global consistency and ease of maintenance.
+
+| Category | Token Name | Value | Description |
+| --- | --- | --- | --- |
+| **Color** | --color-background-primary | #111827 (Dark) / #FFFFFF (Light) | Main app background color. |
+| | --color-background-secondary | #1F2937 (Dark) / #F3F4F6 (Light) | Secondary background for cards, modals. |
+| | --color-text-primary | #F9FAFB (Dark) / #111827 (Light) | Primary text for headings and body. |
+| | --color-text-secondary | #9CA3AF (Dark) / #6B7280 (Light) | Secondary text for subtitles, metadata. |
+| | --color-brand-accent | #8B5CF6 | Primary accent for CTAs, active states, links. |
+| | --color-semantic-success | #10B981 | Used for success messages and positive balances. |
+| | --color-semantic-error | #EF4444 | Used for error messages and destructive actions. |
+| | --color-border-subtle | #374151 (Dark) / #E5E7EB (Light) | Subtle borders for inputs and dividers. |
+| **Typography** | --font-family-primary | 'Inter', sans-serif | The primary font for all UI text. |
+| | --font-size-display | 48px | For large, impactful numbers (e.g., dashboard balance). |
+| | --font-size-h1 | 32px | Screen titles. |
+| | --font-size-h2 | 24px | Section headings. |
+| | --font-size-body | 16px | Main body text, labels. |
+| | --font-size-caption | 12px | Small helper text, metadata. |
+| | --font-weight-regular | 400 | For body and caption text. |
+| | --font-weight-medium | 500 | For labels and secondary buttons. |
+| | --font-weight-semibold | 600 | For headings and active states. |
+| | --font-weight-bold | 700 | For major headings and display text. |
+| **Spacing** | --spacing-xs | 4px | Smallest gap, e.g., between icon and text. |
+| | --spacing-sm | 8px | Small component padding. |
+| | --spacing-md | 16px | Standard padding and margins. |
+| | --spacing-lg | 24px | Gaps between larger sections. |
+| | --spacing-xl | 32px | Gaps between major screen regions. |
+| **Sizing** | --border-radius-sm | 4px | For small elements like tags. |
+| | --border-radius-md | 8px | For buttons and input fields. |
+| | --border-radius-lg | 16px | For cards and modals. |
+| | --touch-target-min | 44px | Minimum height/width for all interactive elements. |
+
+## **Section 4: Core Component Blueprint: The Building Blocks of the Interface**
+
+With the design system tokens established, this section defines the appearance, behavior, and states of the primary reusable components. Building the UI from these standardized blocks is essential for maintaining consistency and development efficiency.
+
+### **4.1. Buttons and Calls-to-Action (CTAs)**
+
+Buttons are the primary interactive elements guiding users through tasks. Their design must clearly communicate their function and hierarchy. All buttons must have a minimum touch target size of 44x44px to be easily tappable, a principle derived from studies on fingertip size.
+
+- **Primary Button:** Reserved for the single most important action on a screen, such as "Add Expense" or "Settle Up." It features a solid background fill using the vibrant brand accent color (--color-brand-accent) to draw maximum attention. The text is white for high contrast.
+- **Secondary Button:** Used for less critical actions, like "Edit" or "Cancel." It has a more subdued appearance, featuring a transparent background with a 1px border in the accent color, or a light gray background (--color-background-secondary).
+- **Tertiary/Text Button:** For low-priority or supplementary actions, such as "View Details." It appears as plain text in the accent color, without a border or background fill, minimizing its visual weight.
+- **Floating Action Button (FAB):** A circular, elevated button that can be used for a persistent, context-aware primary action, like adding a new expense from the main dashboard. It should be placed in the bottom-right "thumb zone" for easy one-handed access.
+- **States:** All buttons must have clearly defined visual states to provide immediate feedback to the user. This includes default, pressed (e.g., slightly darker or scaled down), disabled (e.g., reduced opacity, non-interactive), and loading (e.g., replacing the text/icon with a spinner).
+
+### **4.2. Input Fields and Forms**
+
+Forms are a critical point of interaction and a potential source of user friction. The design must prioritize speed, clarity, and ease of use, especially on mobile devices.
+
+- **Design:** A minimalist aesthetic will be applied. Input fields will have a subtle background color (--color-background-secondary) and a thin border (--color-border-subtle). Labels will be placed *above* the input field, a practice that maintains visibility even when the field is active and the keyboard is present. A single-column layout is mandatory to create a clear, linear flow for the user.
+- **Interaction:** When a user taps into a field (on focus), the border and label color will change to the primary accent color (--color-brand-accent). This provides clear visual feedback about the active element.
+- **Validation:** Real-time validation is crucial. As the user types, the input should be checked against requirements. If an error is detected, a descriptive error message (e.g., "Please enter a valid amount") should appear directly below the field in the semantic error color (--color-semantic-error), along with the border turning red. This immediate feedback prevents frustration at the point of submission.
+- **Functionality:** The number of required fields will be minimized to reduce user effort. The app will leverage native mobile features, such as displaying a numeric keypad for amount entry or using a date picker for date selection, to streamline the input process.
+
+### **4.3. Cards (Expense, Group, and Balance Summaries)**
+
+Cards are the primary containers for displaying grouped, digestible pieces of information. They are fundamental to organizing the dashboard and various list views.
+
+- **Design:** Cards will be the main canvas for the **Strategic Glassmorphism** effect. Their background will be semi-transparent and blurred, creating a modern, layered appearance that makes them feel like they are floating above the app's main background. They will have a generous border radius (--border-radius-lg) for a soft, friendly look.
+- **Content and Hierarchy:** Within each card, a strong visual hierarchy is essential to make the information scannable.
+- **Amounts:** Displayed using a large font size and bold weight (--font-size-display, --font-weight-bold).
+- **Descriptions/Names:** Displayed in a standard body font (--font-size-body, --font-weight-medium).
+- **Metadata (Date, Category):** Displayed in a smaller, lighter font (--font-size-caption, --font-weight-regular, --color-text-secondary).
+- **Icons:** Simple, universally understood icons will be used to represent expense categories.
+- **Spacing:** Ample internal padding (--spacing-md or --spacing-lg) will be used within each card to prevent a cluttered feel and ensure the content is easy to read.
+
+### **4.4. Navigation Elements**
+
+Consistent and intuitive navigation is the backbone of a usable application. The design will rely on established, platform-agnostic patterns that users will immediately recognize.
+
+- **Tab Bar:** A bottom tab bar will serve as the primary global navigation. It will contain three to five top-level destinations (e.g., "Dashboard," "Groups," "Activity," "Profile"). This placement is optimal for one-handed use on mobile devices. The active tab will be clearly differentiated using a filled icon and a semibold text label, while inactive tabs will use outlined icons and regular weight text.
+- **Headers:** Screen headers will be clean and focused. The title of the current screen will be displayed prominently using a large, bold font (--font-size-h1, --font-weight-bold), reinforcing the user's location within the app. Action icons (e.g., for search or settings) will be placed on the right and kept to a minimum to avoid clutter.
+
+The following table provides a visual and descriptive specification for the variants and states of key interactive components.
+
+| Component | Variant / State | Visual Description | Specifications |
+| --- | --- | --- | --- |
+| **Button** | **Primary** | Solid fill, rounded corners, bold text. | background: var(--color-brand-accent); color: white; |
+| | **Primary (Pressed)** | Slightly darker solid fill, scaled down. | background: darker-accent; transform: scale(0.98); |
+| | **Primary (Disabled)** | Grayed-out solid fill, no interaction. | background: var(--color-text-secondary); opacity: 0.5; |
+| | **Secondary** | Transparent fill, colored border, medium text. | background: transparent; border: 1px solid var(--color-brand-accent); |
+| | **Secondary (Pressed)** | Subtle background fill. | background: rgba(accent, 0.1); |
+| **Input Field** | **Default** | Subtle background, thin gray border, label above. | background: var(--color-background-secondary); border: 1px solid var(--color-border-subtle); |
+| | **Focused** | Border and label change to accent color. | border-color: var(--color-brand-accent); label-color: var(--color-brand-accent); |
+| | **Error** | Red border, red label, red error message below. | border-color: var(--color-semantic-error); label-color: var(--color-semantic-error); |
+| **Tab Bar Item** | **Inactive** | Outlined icon, regular weight label in secondary text color. | icon: outline; color: var(--color-text-secondary); |
+| | **Active** | Filled icon, semibold label in primary text color. | icon: solid; color: var(--color-text-primary); |
+
+## **Section 5: Screen-by-Screen Redesign: Reimagining the User Journey**
+
+This section applies the established design system and components to the application's most critical screens. The goal is to create a user journey that is intuitive, efficient, and visually cohesive.
+
+### **5.1. Onboarding: First Impressions and Trust Building**
+
+The onboarding process is the user's first interaction with the app and is critical for setting the tone, communicating value, and establishing trust. The flow will be short, visually engaging, and focused on getting the user to the core functionality as quickly as possible.
+
+- **Flow:** A three-to-four screen carousel will use bold typography and minimalist illustrations to quickly communicate the app's primary benefits: "Split bills easily," "Track group expenses," and "Settle up simply."
+- **Security First:** The sign-up/login screen will immediately build trust by visually communicating security. This will be achieved through the use of familiar padlock icons, clear language about data protection, and prominent options for biometric login (Face ID/fingerprint), which users associate with high security. The process will be streamlined, asking for only the essential information to create an account.
+
+### **5.2. The Dashboard: Your Financial Snapshot at a Glance**
+
+The dashboard is the user's home base and must provide an immediate, at-a-glance overview of their financial standing within the app. The design will prioritize clarity and quick access to the most common action.
+
+- **Layout:** The screen will be dominated by a large summary card at the top, using the Glassmorphism style. This card will clearly display the user's net balance, broken down into two key figures: "You are owed" (in green) and "You owe" (in a neutral or slightly negative color). This clear hierarchy immediately answers the user's most important question.
+- **Activity Feed:** Below the summary card, a vertically scrolling feed will display recent activities (new expenses, settlements) using the standardized Expense Card component. This provides context and a quick way to review recent transactions.
+- **Primary CTA:** A Floating Action Button (FAB) with a plus icon will be persistently located in the bottom-right corner. This thumb-friendly placement ensures that the app's most frequent action—adding a new expense—is always just one tap away, regardless of scroll position. This design is inspired by the clear, action-oriented layouts of modern fintech apps like Monzo and other contemporary dashboard designs.
+
+### **5.3. Adding an Expense: A Frictionless Flow**
+
+This is the most critical user flow in the application. It must be optimized for maximum speed and minimum friction. The goal is to make logging an expense feel effortless, almost instantaneous.
+
+- **Presentation:** Tapping the "Add Expense" FAB will trigger a modal bottom sheet to slide up from the bottom of the screen. This interaction feels faster and less disruptive than navigating to a full new page.
+- **Input-First Design:** The screen's primary focus will be a large, clear input field for the expense amount. Upon opening the sheet, the numeric keypad will automatically appear, allowing the user to begin typing immediately.
+- **Streamlined Selections:** Below the amount, simple, tappable rows with icons will allow the user to add a description, select the group or friends involved, and choose a category. The flow will use smart defaults to minimize taps; for instance, the split method will default to "equally," the most common use case. The entire process of adding a simple, equally split expense should be achievable in just a few seconds, directly addressing a common pain point in less optimized expense trackers.
+
+### **5.4. Group Details: Collaborative Clarity**
+
+This screen provides a centralized view of all expenses and balances within a specific group. The design must balance a comprehensive overview with a clear, uncluttered presentation.
+
+- **Header and Summary:** The header will prominently display the group's name and an optional custom image. Below this, a summary section will show the current balance for each group member, clearly indicating who is in debt and who is owed money. Tapping on a member's avatar will provide a more detailed breakdown of their transactions within the group.
+- **Expense List:** The main body of the screen will be a chronological list of all expenses shared within the group, with each item rendered using the standard Expense Card component. This provides a consistent and easily scannable history of the group's activity.
+- **Actions:** Two clear CTAs will be present: a primary button to "Add Expense" (which would pre-populate the expense with the current group) and a secondary button to "Settle Up," initiating the settlement flow for the group.
+
+### **5.5. Settling Up: Simple and Secure Transactions**
+
+The settlement flow is a moment of high importance where user trust is paramount. The design must be exceptionally clear, simple, and reassuring.
+
+- **Clear Intent:** The screen will use direct, unambiguous language, such as "You pay [Friend's Name] $[Amount]." The use of avatars and full names reinforces who the payment is directed to.
+- **Payment Options:** The interface will provide clear options for how the settlement is being made. This includes a primary option to "Record a cash payment" (which simply marks the debt as paid within the app) and could potentially integrate with third-party payment services.
+- **Visual Trust Cues:** The design will leverage the "Fintech Trust" color palette (blues and greens) and security-related icons (e.g., a shield or lock) to visually reassure the user that the transaction is being handled securely and accurately, a critical practice in all financial applications.
+
+## **Section 6: Bringing the Interface to Life: Motion, Animation, and Microinteractions**
+
+In modern mobile applications, motion design is not a decorative flourish; it is a functional tool that enhances the user experience. Thoughtfully implemented animations provide feedback, guide user attention, and create a sense of polish and delight that makes the app feel alive and responsive. The philosophy is to use motion that is subtle, fast, and meaningful.
+
+### **6.1. The Philosophy of Motion: Feedback, Guidance, and Delight**
+
+Every animation must serve a purpose. The motion system will be built on three pillars:
+
+- **Feedback:** Microinteractions provide immediate confirmation that the system has registered a user's input. When a user taps a button, it should visually react. When they pull to refresh a list, a loading indicator should appear and animate. This constant feedback loop is essential for building user confidence and preventing uncertainty.
+- **Guidance:** Motion can be used to orient users and clarify spatial relationships within the app. For example, when navigating to a new screen, it should slide in from the right, reinforcing the mental model of moving forward in a hierarchy. When a modal appears, it should slide up from the bottom, indicating a temporary, overlayed context. These transitions make navigation feel more intuitive and less jarring than abrupt screen changes.
+- **Delight:** While most animations should be functional and subtle, there are opportunities to use motion to create moments of positive emotional connection. A small, celebratory animation—like confetti or a cheerful icon—when a group's balance is fully settled can transform a mundane task into a rewarding experience. This technique is used effectively by apps like Asana and Mailchimp to encourage desired behaviors.
+
+### **6.2. Key Animation Specifications**
+
+To ensure a consistent and high-performance motion system, specific animation properties will be defined for common interactions. Durations will be kept short (typically between 150ms and 300ms) to ensure the interface feels snappy and responsive.
+
+- **Screen Transitions:** Instead of instantaneous screen changes, use a subtle fade combined with a horizontal slide transition. This creates a smoother, more connected navigational flow.
+- **Loading States:** For loading content, implement **skeleton screens**. These are UI placeholders that mimic the layout of the content that is about to appear. This approach manages user expectation and makes the app feel significantly faster and more responsive than displaying a generic, indeterminate spinner.
+- **Button Interactions:** Taps on all interactive elements should trigger a quick (100-150ms) transform: scale(0.98) effect. This provides tactile, instantaneous feedback that the press was registered.
+- **FAB Animation:** To prevent the Floating Action Button from obscuring content on scrollable screens, it can be animated to subtly shrink or slide off-screen as the user scrolls down, and then reappear as they scroll up. This makes the layout feel more dynamic and considerate of the user's focus.
+
+The following table provides detailed specifications for key microinteractions, serving as a direct guide for implementation.
+
+| Interaction ID | Trigger | Target Element | Animation Properties | Duration | Easing Curve | Notes |
+| --- | --- | --- | --- | --- | --- | --- |
+| primary-cta-press | Tap & Hold | Primary Button | transform: scale(0.98) | 150ms | ease-out | Provides immediate tactile feedback on press. |
+| screen-nav-forward | Tap Navigation Link | New Screen View | opacity: 0 -> 1, transform: translateX(20%) -> translateX(0) | 300ms | cubic-bezier(0.4, 0, 0.2, 1) | Creates a smooth forward navigation flow. |
+| modal-open | Tap Element | Modal/Bottom Sheet | transform: translateY(100%) -> translateY(0) | 250ms | ease-out | Indicates a temporary overlay context. |
+| list-item-add | Add Expense | New Card in List | opacity: 0 -> 1, transform: scale(0.9) | 200ms | ease-out | New item gracefully appears in the feed. |
+| pull-to-refresh | Pull Down List | Loading Indicator | Rotation and opacity animations. | N/A | linear | Standard feedback for data refresh action. |
+| settle-up-success | Settle Final Debt | Success Modal | Confetti/Particle Animation | 1500ms | ease-out | A moment of delight for completing a major task. |
+
+## **Section 7: Strategic Recommendations and Implementation Roadmap**
+
+A successful redesign requires not only a strong blueprint but also a strategic approach to implementation and maintenance. This final section provides recommendations to ensure a smooth rollout and the long-term integrity of the new design.
+
+### **7.1. Phased Rollout Strategy**
+
+Attempting to overhaul the entire application at once can be risky and resource-intensive. A phased approach is recommended to manage complexity and gather user feedback iteratively.
+
+1. **Phase 1: Build the Foundation.** The first and most critical step is to implement the Design System. This involves creating the reusable core components (Buttons, Input Fields, Cards) and defining the global styles (color tokens, typography scale) in the React Native codebase. This foundational work will accelerate all subsequent development.
+2. **Phase 2: Target the Core Flow.** Once the component library is in place, apply the new design to the single most critical user journey: adding a new expense. This flow is high-frequency and central to the app's value. Releasing a redesigned version of this flow first allows for focused user testing and validation.
+3. **Phase 3: Redesign Key Screens.** Following the successful rollout of the core flow, proceed to redesign the other primary screens as defined in Section 5: the Dashboard, Group Details, and Settlement screens.
+4. **Phase 4: Address Ancillary Screens.** Finally, apply the design system to all remaining screens, such as settings, user profiles, and notifications, to ensure a fully cohesive experience.
+
+### **7.2. A/B Testing Opportunities**
+
+Data should inform final design decisions wherever possible. The new design presents several opportunities for A/B testing to optimize for user engagement and conversion:
+
+- **"Add Expense" CTA:** Test the effectiveness of a persistent Floating Action Button (FAB) against a prominent button placed within the bottom tab bar. Measure which placement leads to a higher frequency of expense creation.
+- **Accent Color:** Test two or three different options from the vibrant accent palette (e.g., Purple vs. Aqua) as the default theme. Analyze user retention and subjective feedback to see if one color is perceived more positively.
+- **Onboarding Copy:** Test different headlines and value propositions in the onboarding flow to determine which messaging results in the highest completion rate for new user sign-ups.
+
+### **7.3. Maintaining Design Consistency**
+
+The long-term success of this redesign hinges on maintaining design consistency as the application evolves. The Design System is the primary tool for achieving this.
+
+- **Strict Adherence:** All new features and screens must be constructed using the established components and design tokens. Developers and designers should resist the temptation to create one-off styles or components for new features, as this leads to design debt and a fragmented user experience.
+- **Governance:** Appoint a clear owner or small team responsible for maintaining and evolving the design system. This includes updating components, adding new ones as needed, and ensuring all changes are documented and communicated to the entire development team. This structured approach is a key practice of successful, scalable fintech products like Revolut and Monzo.
+
+### **7.4. Future-Proofing the Design**
+
+This blueprint establishes a modern and flexible foundation. However, the digital landscape is constantly evolving. The design system should be viewed as a living document, capable of incorporating future innovations.
+
+- **Voice UI:** The streamlined "Add Expense" flow is well-suited for future integration with a voice user interface. A user could simply say, "Add a $20 expense for coffee with Jane," and the system would parse the command using the same underlying logic.
+- **Augmented Reality:** As AR technology becomes more commonplace, features like receipt scanning could be enhanced. A user could point their camera at a receipt, and the app could use AR to overlay the parsed line items directly onto the physical receipt in real-time, ready for splitting.
+- **Enhanced Personalization:** The system can be expanded to include more advanced personalization, such as AI-powered suggestions for expense categorization or dynamic dashboard widgets that adapt to a user's specific spending habits.
+
+By building on this robust and modern design foundation, the Splitwiser application will be well-positioned not only to meet the expectations of Gen Z today but also to adapt and thrive in the future.
\ No newline at end of file
diff --git a/frontend/App.js b/frontend/App.js
index f5496adf..21be720e 100644
--- a/frontend/App.js
+++ b/frontend/App.js
@@ -1,13 +1,16 @@
-import React from 'react';
-import AppNavigator from './navigation/AppNavigator';
import { PaperProvider } from 'react-native-paper';
import { AuthProvider } from './context/AuthContext';
+import AppNavigator from './navigation/AppNavigator';
+import { paperTheme } from './utils/theme';
+import { ToastProvider } from './utils/toast';
export default function App() {
return (
-
-
+
+
+
+
);
diff --git a/frontend/components/core/Button.js b/frontend/components/core/Button.js
new file mode 100644
index 00000000..5c5b9944
--- /dev/null
+++ b/frontend/components/core/Button.js
@@ -0,0 +1,182 @@
+// Core Button Component - Following Blueprint Specifications
+// Implements the 8-second rule and haptic feedback for Gen Z engagement
+
+import * as Haptics from 'expo-haptics';
+import { LinearGradient } from 'expo-linear-gradient';
+import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
+import theme, { borderRadius, colors, shadows, spacing } from '../../utils/theme';
+
+const Button = ({
+ title,
+ variant = 'primary', // primary, secondary, outline, ghost, destructive
+ size = 'medium', // small, medium, large
+ onPress,
+ disabled = false,
+ loading = false,
+ icon,
+ fullWidth = false,
+ style,
+ textStyle,
+ ...props
+}) => {
+ const handlePress = async () => {
+ if (disabled || loading) return;
+
+ // Haptic feedback for engagement (Gen Z preference for tactile response)
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+
+ if (onPress) {
+ onPress();
+ }
+ };
+
+ // Size configurations following minimum touch target of 44px
+ const sizeConfig = {
+ small: {
+ paddingVertical: spacing.sm,
+ paddingHorizontal: spacing.md,
+ minHeight: 36,
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ medium: {
+ paddingVertical: spacing.md,
+ paddingHorizontal: spacing.lg,
+ minHeight: 44, // Accessibility minimum
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ large: {
+ paddingVertical: spacing.lg,
+ paddingHorizontal: spacing.xl,
+ minHeight: 52,
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ };
+
+ // Variant configurations for different button types
+ const variantConfig = {
+ primary: {
+ useGradient: true,
+ gradientColors: [colors.brand.accent, colors.brand.accentAlt],
+ textColor: '#FFFFFF',
+ shadowStyle: shadows.small,
+ },
+ secondary: {
+ backgroundColor: colors.background.secondary,
+ textColor: colors.text.primary,
+ borderWidth: 1,
+ borderColor: colors.border.subtle,
+ shadowStyle: shadows.subtle,
+ },
+ outline: {
+ backgroundColor: 'transparent',
+ textColor: colors.brand.accent,
+ borderWidth: 2,
+ borderColor: colors.brand.accent,
+ },
+ ghost: {
+ backgroundColor: 'transparent',
+ textColor: colors.brand.accent,
+ },
+ destructive: {
+ backgroundColor: colors.semantic.error,
+ textColor: '#FFFFFF',
+ shadowStyle: shadows.small,
+ },
+ };
+
+ const currentSize = sizeConfig[size];
+ const currentVariant = variantConfig[variant];
+
+ // Base button style
+ const buttonStyle = {
+ borderRadius: borderRadius.md,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexDirection: 'row',
+ minHeight: currentSize.minHeight,
+ paddingVertical: currentSize.paddingVertical,
+ paddingHorizontal: currentSize.paddingHorizontal,
+ width: fullWidth ? '100%' : 'auto',
+ opacity: disabled ? 0.6 : 1,
+ ...currentVariant.shadowStyle,
+ ...currentVariant,
+ ...style,
+ };
+
+ // Text style
+ const textStyleConfig = {
+ fontSize: currentSize.fontSize,
+ fontWeight: currentSize.fontWeight,
+ color: currentVariant.textColor,
+ fontFamily: 'Inter',
+ ...textStyle,
+ };
+
+ // Loading spinner color
+ const spinnerColor = currentVariant.textColor;
+
+ const ButtonContent = () => (
+
+ {loading && (
+
+ )}
+ {icon && !loading && (
+
+ {icon}
+
+ )}
+
+ {title}
+
+
+ );
+
+ // Render with gradient if specified
+ if (currentVariant.useGradient && !disabled) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ // Regular button without gradient
+ return (
+
+
+
+ );
+};
+
+export default Button;
+export { Button as ModernButton };
diff --git a/frontend/components/core/Input.js b/frontend/components/core/Input.js
new file mode 100644
index 00000000..ef9fd80d
--- /dev/null
+++ b/frontend/components/core/Input.js
@@ -0,0 +1,320 @@
+// Enhanced Input Components - Following Blueprint Specifications
+// Implements glassmorphism and tactile feedback for modern UX
+
+import * as Haptics from 'expo-haptics';
+import { useRef, useState } from 'react';
+import {
+ Animated,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ View
+} from 'react-native';
+import theme, { animations, borderRadius, colors, spacing, typography } from '../../utils/theme';
+
+const EnhancedTextInput = ({
+ label,
+ placeholder,
+ value,
+ onChangeText,
+ error,
+ helperText,
+ leftIcon,
+ rightIcon,
+ secureTextEntry = false,
+ keyboardType = 'default',
+ multiline = false,
+ numberOfLines = 1,
+ disabled = false,
+ style,
+ inputStyle,
+ variant = 'standard', // standard, filled, outlined
+ autoCapitalize = 'sentences',
+ ...props
+}) => {
+ const [isFocused, setIsFocused] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const animatedValue = useRef(new Animated.Value(0)).current;
+
+ const handleFocus = async () => {
+ setIsFocused(true);
+ // Haptic feedback on focus for tactile engagement
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+
+ Animated.timing(animatedValue, {
+ toValue: 1,
+ duration: animations.timing.fast,
+ useNativeDriver: false,
+ }).start();
+ };
+
+ const handleBlur = () => {
+ setIsFocused(false);
+ Animated.timing(animatedValue, {
+ toValue: 0,
+ duration: animations.timing.fast,
+ useNativeDriver: false,
+ }).start();
+ };
+
+ const togglePasswordVisibility = async () => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ setShowPassword(!showPassword);
+ };
+
+ // Animated border color based on focus state
+ const borderColor = animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [colors.border.subtle, colors.brand.accent],
+ });
+
+ // Base input container style
+ const containerStyle = {
+ marginVertical: spacing.sm,
+ ...style,
+ };
+
+ // Input variant styles
+ const getInputContainerStyle = () => {
+ const baseStyle = {
+ flexDirection: 'row',
+ alignItems: multiline ? 'flex-start' : 'center',
+ borderRadius: borderRadius.md,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.md,
+ minHeight: 44, // Accessibility minimum
+ opacity: disabled ? 0.6 : 1,
+ };
+
+ switch (variant) {
+ case 'filled':
+ return {
+ ...baseStyle,
+ backgroundColor: colors.glass.background,
+ borderWidth: 1,
+ borderColor: colors.glass.border,
+ };
+ case 'outlined':
+ return {
+ ...baseStyle,
+ backgroundColor: 'transparent',
+ borderWidth: 2,
+ borderColor: error ? colors.semantic.error : colors.border.subtle,
+ };
+ default: // standard
+ return {
+ ...baseStyle,
+ backgroundColor: colors.background.secondary,
+ borderWidth: 1,
+ borderColor: error ? colors.semantic.error : colors.border.subtle,
+ };
+ }
+ };
+
+ // Text input style
+ const textInputStyle = {
+ flex: 1,
+ fontSize: typography.body.fontSize,
+ fontFamily: typography.body.fontFamily,
+ color: colors.text.primary,
+ paddingTop: multiline ? spacing.sm : 0,
+ textAlignVertical: multiline ? 'top' : 'center',
+ ...inputStyle,
+ };
+
+ return (
+
+ {/* Label */}
+ {label && (
+
+ {label}
+
+ )}
+
+ {/* Input Container with Animated Border */}
+
+ {/* Left Icon */}
+ {leftIcon && (
+
+ {leftIcon}
+
+ )}
+
+ {/* Text Input */}
+
+
+ {/* Right Icon or Password Toggle */}
+ {(rightIcon || secureTextEntry) && (
+
+ {secureTextEntry ? (
+
+ {showPassword ? '👁️' : '👁️🗨️'}
+
+ ) : (
+ rightIcon
+ )}
+
+ )}
+
+
+ {/* Error Message */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Helper Text */}
+ {helperText && !error && (
+
+ {helperText}
+
+ )}
+
+ );
+};
+
+// Currency Input Component for financial amounts
+const CurrencyInput = ({
+ label = 'Amount',
+ value,
+ onChangeText,
+ currency = '$',
+ placeholder = '0.00',
+ error,
+ ...props
+}) => {
+ const formatCurrency = (text) => {
+ // Remove non-numeric characters except decimal point
+ const cleaned = text.replace(/[^0-9.]/g, '');
+
+ // Ensure only one decimal point
+ const parts = cleaned.split('.');
+ if (parts.length > 2) {
+ return parts[0] + '.' + parts.slice(1).join('');
+ }
+
+ // Limit to 2 decimal places
+ if (parts[1] && parts[1].length > 2) {
+ return parts[0] + '.' + parts[1].substring(0, 2);
+ }
+
+ return cleaned;
+ };
+
+ const handleChange = (text) => {
+ const formatted = formatCurrency(text);
+ onChangeText(formatted);
+ };
+
+ return (
+
+ {currency}
+
+ }
+ {...props}
+ />
+ );
+};
+
+// Search Input Component
+const SearchInput = ({
+ placeholder = 'Search...',
+ value,
+ onChangeText,
+ onClear,
+ ...props
+}) => {
+ const handleClear = async () => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ onChangeText('');
+ if (onClear) onClear();
+ };
+
+ return (
+ 🔍
+ }
+ rightIcon={
+ value ? (
+
+
+ ✕
+
+
+ ) : null
+ }
+ {...props}
+ />
+ );
+};
+
+export { CurrencyInput, EnhancedTextInput, SearchInput };
+export default EnhancedTextInput;
diff --git a/frontend/components/navigation/ModernNavigation.js b/frontend/components/navigation/ModernNavigation.js
new file mode 100644
index 00000000..a3b560d5
--- /dev/null
+++ b/frontend/components/navigation/ModernNavigation.js
@@ -0,0 +1,465 @@
+// Enhanced Navigation Components - Following Blueprint Specifications
+// Implements modern tab bar and navigation patterns for Gen Z UX
+
+import * as Haptics from 'expo-haptics';
+import { LinearGradient } from 'expo-linear-gradient';
+import {
+ Dimensions,
+ Text,
+ TouchableOpacity,
+ View
+} from 'react-native';
+import theme, {
+ colors,
+ shadows,
+ spacing,
+ typography
+} from '../../utils/theme';
+
+const { width: screenWidth } = Dimensions.get('window');
+
+// Modern Tab Bar Component
+const ModernTabBar = ({
+ state,
+ descriptors,
+ navigation,
+ style,
+}) => {
+ return (
+
+
+ {state.routes.map((route, index) => {
+ const { options } = descriptors[route.key];
+ const label = options.tabBarLabel !== undefined
+ ? options.tabBarLabel
+ : options.title !== undefined
+ ? options.title
+ : route.name;
+
+ const isFocused = state.index === index;
+
+ const onPress = async () => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+
+ const event = navigation.emit({
+ type: 'tabPress',
+ target: route.key,
+ });
+
+ if (!isFocused && !event.defaultPrevented) {
+ navigation.navigate(route.name);
+ }
+ };
+
+ // Tab configuration
+ const getTabConfig = (routeName) => {
+ switch (routeName) {
+ case 'Home':
+ return { icon: '🏠', label: 'Home' };
+ case 'Groups':
+ return { icon: '👥', label: 'Groups' };
+ case 'AddExpense':
+ return { icon: '➕', label: 'Add', isSpecial: true };
+ case 'Activity':
+ return { icon: '📊', label: 'Activity' };
+ case 'Profile':
+ return { icon: '👤', label: 'Profile' };
+ default:
+ return { icon: '•', label: routeName };
+ }
+ };
+
+ const tabConfig = getTabConfig(route.name);
+
+ // Special handling for Add button (center button)
+ if (tabConfig.isSpecial) {
+ return (
+
+
+
+ {tabConfig.icon}
+
+
+
+ );
+ }
+
+ // Regular tab items
+ return (
+
+ {/* Icon */}
+
+
+ {tabConfig.icon}
+
+
+
+ {/* Label */}
+
+ {tabConfig.label}
+
+
+ {/* Active indicator */}
+ {isFocused && (
+
+ )}
+
+ );
+ })}
+
+
+ );
+};
+
+// Header Component with glassmorphism
+const ModernHeader = ({
+ title,
+ subtitle,
+ leftAction,
+ rightAction,
+ showBackButton = false,
+ navigation,
+ variant = 'default', // default, transparent, gradient
+ style,
+}) => {
+ const handleBackPress = async () => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ navigation?.goBack();
+ };
+
+ const getHeaderStyle = () => {
+ switch (variant) {
+ case 'transparent':
+ return {
+ backgroundColor: 'transparent',
+ };
+ case 'gradient':
+ return {
+ // Will be wrapped in LinearGradient
+ };
+ default:
+ return {
+ backgroundColor: colors.glass.background,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.glass.border,
+ };
+ }
+ };
+
+ const headerContent = (
+
+ {/* Left Section */}
+
+ {showBackButton && (
+
+ ←
+
+ )}
+
+ {leftAction}
+
+
+ {/* Center Section */}
+
+
+ {title}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+
+ {/* Right Section */}
+
+ {rightAction}
+
+
+ );
+
+ if (variant === 'gradient') {
+ return (
+
+ {headerContent}
+
+ );
+ }
+
+ return headerContent;
+};
+
+// Action Button for headers and floating actions
+const ActionButton = ({
+ icon,
+ label,
+ onPress,
+ variant = 'default', // default, primary, ghost
+ size = 'medium', // small, medium, large
+ style,
+}) => {
+ const handlePress = async () => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ onPress?.();
+ };
+
+ const sizeConfig = {
+ small: { width: 32, height: 32, fontSize: 16 },
+ medium: { width: 40, height: 40, fontSize: 20 },
+ large: { width: 48, height: 48, fontSize: 24 },
+ };
+
+ const variantConfig = {
+ default: {
+ backgroundColor: colors.glass.background,
+ borderColor: colors.glass.border,
+ iconColor: colors.text.primary,
+ },
+ primary: {
+ backgroundColor: colors.brand.accent,
+ borderColor: colors.brand.accent,
+ iconColor: '#FFFFFF',
+ },
+ ghost: {
+ backgroundColor: 'transparent',
+ borderColor: 'transparent',
+ iconColor: colors.text.secondary,
+ },
+ };
+
+ const currentSize = sizeConfig[size];
+ const currentVariant = variantConfig[variant];
+
+ return (
+
+ {typeof icon === 'string' ? (
+
+ {icon}
+
+ ) : (
+ icon
+ )}
+
+ {label && (
+
+ {label}
+
+ )}
+
+ );
+};
+
+// Floating Action Button
+const FloatingActionButton = ({
+ icon = '➕',
+ onPress,
+ position = 'bottom-right', // bottom-right, bottom-left, bottom-center
+ style,
+}) => {
+ const handlePress = async () => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
+ onPress?.();
+ };
+
+ const getPositionStyle = () => {
+ const baseStyle = {
+ position: 'absolute',
+ bottom: spacing.xl,
+ zIndex: 1000,
+ };
+
+ switch (position) {
+ case 'bottom-left':
+ return { ...baseStyle, left: spacing.lg };
+ case 'bottom-center':
+ return {
+ ...baseStyle,
+ left: (screenWidth / 2) - 28, // Center minus half button width
+ };
+ default: // bottom-right
+ return { ...baseStyle, right: spacing.lg };
+ }
+ };
+
+ return (
+
+
+
+ {icon}
+
+
+
+ );
+};
+
+export {
+ ActionButton,
+ FloatingActionButton, ModernHeader, ModernTabBar
+};
+
diff --git a/frontend/navigation/GroupsStackNavigator.js b/frontend/navigation/GroupsStackNavigator.js
index 5ede954d..cf3dcd07 100644
--- a/frontend/navigation/GroupsStackNavigator.js
+++ b/frontend/navigation/GroupsStackNavigator.js
@@ -1,9 +1,9 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
-import AddExpenseScreen from '../screens/AddExpenseScreen';
-import GroupDetailsScreen from '../screens/GroupDetailsScreen';
import GroupSettingsScreen from '../screens/GroupSettingsScreen';
import HomeScreen from '../screens/HomeScreen';
import JoinGroupScreen from '../screens/JoinGroupScreen';
+import ModernAddExpenseScreen from '../screens/ModernAddExpenseScreen';
+import ModernGroupDetailsScreen from '../screens/ModernGroupDetailsScreen';
const Stack = createNativeStackNavigator();
@@ -11,10 +11,10 @@ const GroupsStackNavigator = () => {
return (
-
-
+
+
-
+
);
};
diff --git a/frontend/navigation/MainNavigator.js b/frontend/navigation/MainNavigator.js
index dc9d32a2..9b519cc8 100644
--- a/frontend/navigation/MainNavigator.js
+++ b/frontend/navigation/MainNavigator.js
@@ -1,15 +1,21 @@
-import React from 'react';
-import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { MaterialCommunityIcons } from '@expo/vector-icons';
-import GroupsStackNavigator from './GroupsStackNavigator';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { ModernTabBar } from '../components/navigation/ModernNavigation';
import FriendsScreen from '../screens/FriendsScreen';
import AccountStackNavigator from './AccountStackNavigator';
+import GroupsStackNavigator from './GroupsStackNavigator';
const Tab = createBottomTabNavigator();
const MainNavigator = () => {
return (
-
+ }
+ >
{
tabBarIcon: ({ color, size }) => (
),
+ tabBarLabel: 'Groups'
}}
/>
{
tabBarIcon: ({ color, size }) => (
),
+ tabBarLabel: 'Friends'
}}
/>
{
tabBarIcon: ({ color, size }) => (
),
+ tabBarLabel: 'Account'
}}
/>
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7e1481a1..4cee7b5a 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,7 +15,9 @@
"@react-navigation/native-stack": "^7.3.23",
"axios": "^1.11.0",
"expo": "~53.0.20",
+ "expo-haptics": "^14.1.4",
"expo-image-picker": "~16.0.2",
+ "expo-linear-gradient": "^14.1.5",
"expo-status-bar": "~2.2.3",
"react": "19.0.0",
"react-dom": "19.0.0",
@@ -4301,6 +4303,14 @@
"react": "*"
}
},
+ "node_modules/expo-haptics": {
+ "version": "14.1.4",
+ "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-14.1.4.tgz",
+ "integrity": "sha512-QZdE3NMX74rTuIl82I+n12XGwpDWKb8zfs5EpwsnGi/D/n7O2Jd4tO5ivH+muEG/OCJOMq5aeaVDqqaQOhTkcA==",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-image-loader": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.0.0.tgz",
@@ -4330,6 +4340,16 @@
"react": "*"
}
},
+ "node_modules/expo-linear-gradient": {
+ "version": "14.1.5",
+ "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.1.5.tgz",
+ "integrity": "sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ==",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-modules-autolinking": {
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.14.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 39a8d506..5ce176d4 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,7 +16,9 @@
"@react-navigation/native-stack": "^7.3.23",
"axios": "^1.11.0",
"expo": "~53.0.20",
+ "expo-haptics": "^14.1.4",
"expo-image-picker": "~16.0.2",
+ "expo-linear-gradient": "^14.1.5",
"expo-status-bar": "~2.2.3",
"react": "19.0.0",
"react-dom": "19.0.0",
diff --git a/frontend/screens/GroupDetailsScreen.js b/frontend/screens/GroupDetailsScreen.js
index a1050b9f..187e54aa 100644
--- a/frontend/screens/GroupDetailsScreen.js
+++ b/frontend/screens/GroupDetailsScreen.js
@@ -1,12 +1,10 @@
import { useContext, useEffect, useState } from "react";
-import { Alert, FlatList, StyleSheet, Text, View } from "react-native";
+import { Alert, Dimensions, FlatList, StyleSheet, Text, View } from "react-native";
import {
ActivityIndicator,
- Card,
+ Chip,
FAB,
- IconButton,
- Paragraph,
- Title,
+ IconButton
} from "react-native-paper";
import {
getGroupExpenses,
@@ -14,6 +12,11 @@ import {
getOptimizedSettlements,
} from "../api/groups";
import { AuthContext } from "../context/AuthContext";
+import { AnimatedCard, FadeInView, ScaleInView, SlideInView } from "../utils/animations";
+import { GradientCard, StatusGradient } from "../utils/gradients";
+import { borderRadius, colors, shadows, spacing, typography } from "../utils/theme";
+
+const { width } = Dimensions.get('window');
const GroupDetailsScreen = ({ route, navigation }) => {
const { groupId, groupName } = route.params;
@@ -70,36 +73,55 @@ const GroupDetailsScreen = ({ route, navigation }) => {
return member ? member.user.name : "Unknown";
};
- const renderExpense = ({ item }) => {
+ const renderExpense = ({ item, index }) => {
const userSplit = item.splits.find((s) => s.userId === user._id);
const userShare = userSplit ? userSplit.amount : 0;
const paidByMe = (item.paidBy || item.createdBy) === user._id;
const net = paidByMe ? item.amount - userShare : -userShare;
let balanceText;
- let balanceColor = "black";
+ let statusType = 'settled';
if (net > 0) {
- balanceText = `You are owed ${formatCurrency(net)}`;
- balanceColor = "green";
+ balanceText = `💰 You're owed ${formatCurrency(net)}`;
+ statusType = 'success';
} else if (net < 0) {
- balanceText = `You borrowed ${formatCurrency(Math.abs(net))}`;
- balanceColor = "red";
+ balanceText = `💳 You borrowed ${formatCurrency(Math.abs(net))}`;
+ statusType = 'warning';
} else {
- balanceText = "You are settled for this expense.";
+ balanceText = "✨ You're settled for this expense";
+ statusType = 'settled';
}
return (
-
-
- {item.description}
- Amount: {formatCurrency(item.amount)}
-
- Paid by: {getMemberName(item.paidBy || item.createdBy)}
-
- {balanceText}
-
-
+
+
+
+
+ {item.description}
+
+ {formatCurrency(item.amount)}
+
+
+
+ Paid by {getMemberName(item.paidBy || item.createdBy)}
+
+
+
+
+
+ {balanceText}
+
+
+
+
);
};
@@ -112,58 +134,63 @@ const GroupDetailsScreen = ({ route, navigation }) => {
// If user is all settled up
if (userOwes.length === 0 && userIsOwed.length === 0) {
return (
-
- ✓ You are all settled up!
-
+
+ 🎉 You're all settled up!
+
+ No pending payments in this group
+
+
);
}
return (
- {/* You owe section - only show if totalOwed > 0 */}
+ {/* You owe section */}
{totalOwed > 0 && (
-
-
- You need to pay:{" "}
- {formatCurrency(totalOwed)}
-
+
+
+
+ 💳 You need to pay
+
+
+ {formatCurrency(totalOwed)}
+
+
{userOwes.map((s, index) => (
-
-
- {getMemberName(s.toUserId)}
-
-
- {formatCurrency(s.amount)}
-
-
+
+ {getMemberName(s.toUserId)}
+
+
+ {formatCurrency(s.amount)}
+
))}
-
+
)}
- {/* You receive section - only show if totalToReceive > 0 */}
+ {/* You receive section */}
{totalToReceive > 0 && (
-
-
- You will receive:{" "}
-
+
+
+
+ 💰 You'll receive
+
+
{formatCurrency(totalToReceive)}
-
+
{userIsOwed.map((s, index) => (
-
-
- {getMemberName(s.fromUserId)}
-
-
- {formatCurrency(s.amount)}
-
-
+
+ {getMemberName(s.fromUserId)}
+
+
+ {formatCurrency(s.amount)}
+
))}
-
+
)}
);
@@ -171,23 +198,32 @@ const GroupDetailsScreen = ({ route, navigation }) => {
if (isLoading) {
return (
-
-
+
+
+
+ Loading group details...
+
);
}
const renderHeader = () => (
- <>
-
-
- Settlement Summary
- {renderSettlementSummary()}
-
-
+
+
+ Settlement Summary
+ {renderSettlementSummary()}
+
- Expenses
- >
+
+ Recent Expenses
+
+ {expenses.length} expense{expenses.length !== 1 ? 's' : ''}
+
+
+
);
return (
@@ -198,17 +234,28 @@ const GroupDetailsScreen = ({ route, navigation }) => {
renderItem={renderExpense}
keyExtractor={(item) => item._id}
ListHeaderComponent={renderHeader}
+ showsVerticalScrollIndicator={false}
ListEmptyComponent={
- No expenses recorded yet.
+
+ No expenses yet! 💸
+
+ Add your first expense to start tracking group spending
+
+
}
- contentContainerStyle={{ paddingBottom: 80 }} // To avoid FAB overlap
+ contentContainerStyle={{ paddingBottom: 100 }}
/>
- navigation.navigate("AddExpense", { groupId: groupId })}
- />
+
+ navigation.navigate("AddExpense", { groupId: groupId })}
+ color="white"
+ label="Add Expense"
+ extended={true}
+ />
+
);
};
@@ -216,99 +263,184 @@ const GroupDetailsScreen = ({ route, navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
+ backgroundColor: colors.background,
},
contentContainer: {
flex: 1,
- padding: 16,
+ padding: spacing.md,
},
loaderContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
- card: {
- marginBottom: 16,
+ loadingText: {
+ ...typography.body1,
+ color: colors.onSurfaceVariant,
+ marginTop: spacing.md,
+ },
+ summaryCard: {
+ marginBottom: spacing.lg,
+ padding: spacing.lg,
+ },
+ summaryTitle: {
+ ...typography.h3,
+ color: 'white',
+ marginBottom: spacing.md,
+ textAlign: 'center',
+ fontWeight: '700',
+ },
+ expensesSectionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: spacing.md,
+ paddingHorizontal: spacing.xs,
},
expensesTitle: {
- marginTop: 16,
- marginBottom: 8,
- fontSize: 20,
- fontWeight: "bold",
+ ...typography.h3,
+ color: colors.onSurface,
},
- memberText: {
- fontSize: 16,
- lineHeight: 24,
+ expensesSubtitle: {
+ ...typography.body2,
+ color: colors.onSurfaceVariant,
},
- fab: {
- position: "absolute",
- margin: 16,
- right: 0,
- bottom: 0,
+ expenseWrapper: {
+ marginBottom: spacing.md,
+ },
+ expenseCard: {
+ backgroundColor: colors.surface,
+ borderRadius: borderRadius.lg,
+ padding: spacing.lg,
+ ...shadows.small,
+ borderWidth: 1,
+ borderColor: colors.outlineVariant,
+ },
+ expenseHeader: {
+ marginBottom: spacing.md,
+ },
+ expenseMainInfo: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ marginBottom: spacing.sm,
+ },
+ expenseTitle: {
+ ...typography.h4,
+ color: colors.onSurface,
+ flex: 1,
+ marginRight: spacing.md,
+ },
+ expenseAmount: {
+ ...typography.amount,
+ color: colors.primary,
+ },
+ paidByChip: {
+ backgroundColor: colors.surfaceVariant,
+ borderColor: colors.outline,
+ },
+ chipText: {
+ ...typography.caption,
+ color: colors.onSurfaceVariant,
+ },
+ expenseStatusContainer: {
+ borderRadius: borderRadius.sm,
+ paddingVertical: spacing.sm,
+ paddingHorizontal: spacing.md,
+ },
+ expenseStatusText: {
+ ...typography.label,
+ color: 'white',
+ textAlign: 'center',
+ fontWeight: '600',
},
// Settlement Summary Styles
settlementContainer: {
- marginBottom: 16,
+ gap: spacing.md,
},
settledContainer: {
- alignItems: "center",
- paddingVertical: 12,
+ paddingVertical: spacing.lg,
+ alignItems: 'center',
},
settledText: {
- fontSize: 16,
- color: "#2e7d32",
- fontWeight: "500",
+ ...typography.h4,
+ color: 'white',
+ fontWeight: '700',
+ textAlign: 'center',
+ marginBottom: spacing.xs,
+ },
+ settledSubtext: {
+ ...typography.body2,
+ color: 'rgba(255, 255, 255, 0.8)',
+ textAlign: 'center',
},
owedSection: {
- backgroundColor: "#ffebee",
- borderRadius: 8,
- padding: 12,
- borderLeftWidth: 4,
- borderLeftColor: "#d32f2f",
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
},
receiveSection: {
- backgroundColor: "#e8f5e8",
- borderRadius: 8,
- padding: 12,
- borderLeftWidth: 4,
- borderLeftColor: "#2e7d32",
+ borderRadius: borderRadius.md,
+ padding: spacing.md,
},
- sectionTitle: {
- fontSize: 16,
- fontWeight: "600",
- marginBottom: 8,
- color: "#333",
+ sectionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: spacing.md,
},
- amountOwed: {
- color: "#d32f2f",
- fontWeight: "bold",
+ sectionTitle: {
+ ...typography.h4,
+ color: 'white',
+ fontWeight: '600',
},
- amountReceive: {
- color: "#2e7d32",
- fontWeight: "bold",
+ totalAmount: {
+ ...typography.amount,
+ color: 'white',
+ fontWeight: '700',
},
settlementItem: {
- marginVertical: 4,
- },
- personInfo: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- paddingVertical: 4,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: spacing.xs,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(255, 255, 255, 0.2)',
+ marginBottom: spacing.xs,
},
personName: {
- fontSize: 14,
- color: "#555",
+ ...typography.body1,
+ color: 'white',
flex: 1,
},
settlementAmount: {
- fontSize: 14,
- fontWeight: "600",
- color: "#333",
+ ...typography.label,
+ color: 'white',
+ fontWeight: '600',
+ },
+ emptyContainer: {
+ alignItems: 'center',
+ paddingTop: spacing.xxl,
+ paddingHorizontal: spacing.lg,
+ },
+ emptyTitle: {
+ ...typography.h2,
+ color: colors.onSurface,
+ textAlign: 'center',
+ marginBottom: spacing.sm,
},
emptyText: {
- fontSize: 14,
- color: "#666",
- paddingVertical: 8,
+ ...typography.body1,
+ color: colors.onSurfaceVariant,
+ textAlign: 'center',
+ lineHeight: 24,
+ },
+ fab: {
+ position: "absolute",
+ bottom: spacing.lg,
+ right: spacing.lg,
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.round,
+ ...shadows.large,
},
});
diff --git a/frontend/screens/GroupSettingsScreen.js b/frontend/screens/GroupSettingsScreen.js
index 90de5d16..e3eb421b 100644
--- a/frontend/screens/GroupSettingsScreen.js
+++ b/frontend/screens/GroupSettingsScreen.js
@@ -12,17 +12,18 @@ import {
ScrollView,
Share,
StyleSheet,
+ TouchableOpacity,
View,
} from "react-native";
import {
ActivityIndicator,
Avatar,
Button,
- Card,
+ Chip,
+ Divider,
IconButton,
- List,
Text,
- TextInput,
+ TextInput
} from "react-native-paper";
import {
deleteGroup as apiDeleteGroup,
@@ -34,6 +35,9 @@ import {
getOptimizedSettlements,
} from "../api/groups";
import { AuthContext } from "../context/AuthContext";
+import { AnimatedCard, FadeInView, SlideInView } from "../utils/animations";
+import { GradientCard, StatusGradient } from "../utils/gradients";
+import { borderRadius, colors, shadows, spacing, typography } from "../utils/theme";
const ICON_CHOICES = ["👥", "🏠", "🎉", "🧳", "🍽️", "🚗", "🏖️", "🎮", "💼"];
@@ -259,168 +263,529 @@ const GroupSettingsScreen = ({ route, navigation }) => {
);
};
- const renderMemberItem = (m) => {
+ const renderMemberItem = (m, index) => {
const isSelf = m.userId === user?._id;
const displayName = m.user?.name || "Unknown";
const imageUrl = m.user?.imageUrl;
+
return (
-
- imageUrl ? (
-
- ) : (
-
- )
- }
- right={() =>
- isAdmin && !isSelf ? (
+
+
+
+ {imageUrl ? (
+
+ ) : (
+
+ )}
+
+ {displayName}
+ {m.role === "admin" && (
+
+ Admin
+
+ )}
+ {isSelf && (
+ You
+ )}
+
+
+ {isAdmin && !isSelf && (
onKick(m.userId, displayName)}
+ style={styles.removeButton}
/>
- ) : null
- }
- />
+ )}
+
+ {index < members.length - 1 && }
+
);
};
if (loading) {
return (
-
-
+
+
+
+ Loading group settings...
+
);
}
return (
-
-
-
-
-
- Icon
-
- {ICON_CHOICES.map((i) => (
+
+ {/* Group Info Section */}
+
+
+ Group Settings
+
+ Manage your group preferences and members
+
+
+
+
+ {/* Basic Info Card */}
+
+
+ 📝 Basic Information
+
+
+ Group Name
+
+
+
+
+ Choose an Icon
+
+
+ {ICON_CHOICES.map((i) => (
+ setIcon(i)}
+ disabled={!isAdmin}
+ >
+ {i}
+
+ ))}
+
+
+
+
+
+ Or Upload Custom Image
+
- ))}
-
-
-
- {pickedImage?.uri ? (
-
- ) : group?.imageUrl &&
- /^(https?:|data:image)/.test(group.imageUrl) ? (
-
- ) : group?.imageUrl ? (
- {group.imageUrl}
- ) : null}
+ {(pickedImage?.uri || (group?.imageUrl && /^(https?:|data:image)/.test(group.imageUrl))) && (
+
+
+ Current
+
+ )}
+
+
{isAdmin && (
)}
-
-
+
+
-
-
- {members.map(renderMemberItem)}
-
+ {/* Members Section */}
+
+
+
+ 👥 Members
+
+ {members.length} member{members.length !== 1 ? 's' : ''}
+
+
+
+
+ {members.map((m, index) => renderMemberItem(m, index))}
+
+
+
-
-
-
-
- Join Code: {group?.joinCode}
+ {/* Invite Section */}
+
+
+ 🎉 Invite Friends
+
+ Share this code with friends to join your group
+
+
+ Join Code
+
+ {group?.joinCode}
+
+
+
-
-
+
+
-
-
-
-
+ {/* Danger Zone */}
+
+
+ ⚠️ Danger Zone
+
+ These actions cannot be undone
+
+
+
+
{isAdmin && (
)}
-
-
+
+
);
};
const styles = StyleSheet.create({
- container: { flex: 1 },
- scrollContent: { padding: 16 },
- loaderContainer: { flex: 1, justifyContent: "center", alignItems: "center" },
- card: { marginBottom: 16 },
- iconRow: { flexDirection: "row", flexWrap: "wrap", marginBottom: 8 },
- iconBtn: { marginRight: 8, marginBottom: 8 },
+ container: {
+ flex: 1,
+ backgroundColor: colors.background
+ },
+ scrollContent: {
+ padding: spacing.lg,
+ paddingBottom: spacing.xxl,
+ },
+ loaderContainer: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center"
+ },
+ loadingText: {
+ ...typography.body1,
+ color: colors.onSurfaceVariant,
+ marginTop: spacing.md,
+ },
+ headerCard: {
+ marginBottom: spacing.lg,
+ padding: spacing.xl,
+ alignItems: 'center',
+ },
+ headerTitle: {
+ ...typography.h2,
+ color: 'white',
+ fontWeight: '700',
+ marginBottom: spacing.xs,
+ },
+ headerSubtitle: {
+ ...typography.body1,
+ color: 'rgba(255, 255, 255, 0.9)',
+ textAlign: 'center',
+ },
+ modernCard: {
+ backgroundColor: colors.surface,
+ borderRadius: borderRadius.lg,
+ padding: spacing.lg,
+ marginBottom: spacing.lg,
+ ...shadows.medium,
+ borderWidth: 1,
+ borderColor: colors.outlineVariant,
+ },
+ sectionTitle: {
+ ...typography.h3,
+ color: colors.onSurface,
+ marginBottom: spacing.lg,
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: spacing.lg,
+ },
+ inputContainer: {
+ marginBottom: spacing.lg,
+ },
+ inputLabel: {
+ ...typography.label,
+ color: colors.onSurface,
+ marginBottom: spacing.sm,
+ fontWeight: '600',
+ },
+ modernInput: {
+ backgroundColor: colors.surface,
+ },
+ iconSection: {
+ marginBottom: spacing.lg,
+ },
+ iconScrollView: {
+ marginBottom: spacing.sm,
+ },
+ iconRow: {
+ flexDirection: "row",
+ gap: spacing.sm,
+ paddingRight: spacing.lg,
+ },
+ iconButton: {
+ width: 56,
+ height: 56,
+ borderRadius: borderRadius.md,
+ backgroundColor: colors.surfaceVariant,
+ borderWidth: 2,
+ borderColor: colors.outline,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ iconButtonSelected: {
+ backgroundColor: colors.primaryLight,
+ borderColor: colors.primary,
+ },
+ iconText: {
+ fontSize: 24,
+ },
+ imageSection: {
+ marginBottom: spacing.lg,
+ },
+ imageUploadContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: spacing.md,
+ },
+ uploadButton: {
+ borderColor: colors.outline,
+ },
+ uploadButtonContent: {
+ paddingVertical: spacing.xs,
+ },
+ currentImageContainer: {
+ alignItems: 'center',
+ },
+ currentImage: {
+ width: 64,
+ height: 64,
+ borderRadius: borderRadius.lg,
+ borderWidth: 2,
+ borderColor: colors.outline,
+ },
+ currentImageLabel: {
+ ...typography.caption,
+ color: colors.onSurfaceVariant,
+ marginTop: spacing.xs,
+ },
+ saveButton: {
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.md,
+ ...shadows.small,
+ },
+ saveButtonContent: {
+ paddingVertical: spacing.sm,
+ },
+ memberCountChip: {
+ backgroundColor: colors.surfaceVariant,
+ },
+ memberCountText: {
+ ...typography.caption,
+ color: colors.onSurfaceVariant,
+ },
+ membersContainer: {
+ borderRadius: borderRadius.md,
+ backgroundColor: colors.surfaceVariant,
+ padding: spacing.md,
+ },
+ memberItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: spacing.sm,
+ },
+ memberInfo: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ memberAvatar: {
+ marginRight: spacing.md,
+ },
+ memberDetails: {
+ flex: 1,
+ },
+ memberName: {
+ ...typography.h4,
+ color: colors.onSurface,
+ marginBottom: spacing.xs,
+ },
+ adminChip: {
+ alignSelf: 'flex-start',
+ backgroundColor: colors.primaryLight,
+ borderColor: colors.primary,
+ },
+ adminChipText: {
+ ...typography.caption,
+ color: colors.primary,
+ fontWeight: '600',
+ },
+ youLabel: {
+ ...typography.caption,
+ color: colors.onSurfaceVariant,
+ fontStyle: 'italic',
+ },
+ removeButton: {
+ backgroundColor: colors.errorLight,
+ },
+ memberDivider: {
+ backgroundColor: colors.outline,
+ marginVertical: spacing.xs,
+ },
+ inviteCard: {
+ padding: spacing.xl,
+ marginBottom: spacing.lg,
+ alignItems: 'center',
+ },
+ inviteTitle: {
+ ...typography.h3,
+ color: 'white',
+ fontWeight: '700',
+ marginBottom: spacing.xs,
+ },
+ inviteSubtitle: {
+ ...typography.body1,
+ color: 'rgba(255, 255, 255, 0.9)',
+ textAlign: 'center',
+ marginBottom: spacing.lg,
+ },
+ joinCodeContainer: {
+ alignItems: 'center',
+ marginBottom: spacing.lg,
+ },
+ joinCodeLabel: {
+ ...typography.label,
+ color: 'rgba(255, 255, 255, 0.8)',
+ marginBottom: spacing.sm,
+ },
+ joinCodeBox: {
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ paddingHorizontal: spacing.lg,
+ paddingVertical: spacing.md,
+ borderRadius: borderRadius.md,
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 0.3)',
+ },
+ joinCode: {
+ ...typography.h3,
+ color: 'white',
+ fontWeight: '700',
+ letterSpacing: 2,
+ },
+ shareButton: {
+ borderRadius: borderRadius.md,
+ },
+ shareButtonContent: {
+ paddingVertical: spacing.sm,
+ },
+ dangerCard: {
+ borderLeftWidth: 4,
+ borderLeftColor: colors.error,
+ },
+ dangerTitle: {
+ ...typography.h3,
+ color: colors.error,
+ marginBottom: spacing.xs,
+ },
+ dangerSubtitle: {
+ ...typography.body2,
+ color: colors.onSurfaceVariant,
+ marginBottom: spacing.lg,
+ },
+ dangerActions: {
+ gap: spacing.md,
+ },
+ leaveButton: {
+ borderColor: colors.warning,
+ },
+ deleteButton: {
+ backgroundColor: colors.error,
+ borderRadius: borderRadius.md,
+ },
+ deleteButtonContent: {
+ paddingVertical: spacing.sm,
+ },
});
export default GroupSettingsScreen;
diff --git a/frontend/screens/HomeScreen.js b/frontend/screens/HomeScreen.js
index dfb0eadd..816d1071 100644
--- a/frontend/screens/HomeScreen.js
+++ b/frontend/screens/HomeScreen.js
@@ -1,33 +1,77 @@
-import { useContext, useEffect, useState } from "react";
-import { Alert, FlatList, StyleSheet, View } from "react-native";
+// Modern Home Screen - Following Blueprint Specifications
+// Implements dashboard with financial overview, glassmorphism, and Gen Z UX
+
+import * as Haptics from 'expo-haptics';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useContext, useEffect, useRef, useState } from "react";
+import {
+ Alert,
+ Animated,
+ Dimensions,
+ FlatList,
+ RefreshControl,
+ ScrollView,
+ StyleSheet,
+ TouchableOpacity,
+ View,
+} from "react-native";
import {
ActivityIndicator,
- Appbar,
- Avatar,
- Button,
- Card,
Modal,
Portal,
Text,
- TextInput,
} from "react-native-paper";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
-import { formatCurrency, getCurrencySymbol } from "../utils/currency";
+
+// Import modern components
+import Button from '../components/core/Button';
+import { EnhancedTextInput } from '../components/core/Input';
+import { FloatingActionButton, ModernHeader } from '../components/navigation/ModernNavigation';
+import { GlassCard, GroupSummaryCard, QuickActionCard } from '../utils/cards';
+import theme, { borderRadius, colors, spacing, typography } from '../utils/theme';
+
+const { width, height } = Dimensions.get('window');
const HomeScreen = ({ navigation }) => {
const { token, logout, user } = useContext(AuthContext);
const [groups, setGroups] = useState([]);
const [isLoading, setIsLoading] = useState(true);
- const [groupSettlements, setGroupSettlements] = useState({}); // Track settlement status for each group
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [groupSettlements, setGroupSettlements] = useState({});
- // State for the Create Group modal
+ // Create Group modal state
const [modalVisible, setModalVisible] = useState(false);
const [newGroupName, setNewGroupName] = useState("");
const [isCreatingGroup, setIsCreatingGroup] = useState(false);
- const showModal = () => setModalVisible(true);
- const hideModal = () => setModalVisible(false);
+ // Animation refs
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(30)).current;
+ const balanceAnim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ // Initial animations
+ Animated.stagger(100, [
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 400,
+ useNativeDriver: true,
+ }),
+ Animated.timing(slideAnim, {
+ toValue: 0,
+ duration: 400,
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ // Only fetch groups if user is authenticated
+ if (user && token) {
+ fetchGroups();
+ } else {
+ setIsLoading(false); // Stop loading if not authenticated
+ }
+ }, [user, token]); // Add dependencies
// Calculate settlement status for a group
const calculateSettlementStatus = async (groupId, userId) => {
@@ -35,221 +79,391 @@ const HomeScreen = ({ navigation }) => {
const response = await getOptimizedSettlements(groupId);
const settlements = response.data.optimizedSettlements || [];
- // Check if user has any pending settlements
const userOwes = settlements.filter((s) => s.fromUserId === userId);
const userIsOwed = settlements.filter((s) => s.toUserId === userId);
const totalOwed = userOwes.reduce((sum, s) => sum + (s.amount || 0), 0);
- const totalToReceive = userIsOwed.reduce(
- (sum, s) => sum + (s.amount || 0),
- 0
- );
+ const totalToReceive = userIsOwed.reduce((sum, s) => sum + (s.amount || 0), 0);
+
+ const netBalance = totalToReceive - totalOwed;
+ const isSettled = settlements.length === 0;
return {
- isSettled: totalOwed === 0 && totalToReceive === 0,
- owesAmount: totalOwed,
- owedAmount: totalToReceive,
- netBalance: totalToReceive - totalOwed,
+ isSettled,
+ netBalance,
+ totalOwed,
+ totalToReceive,
+ settlements,
};
} catch (error) {
- console.error(
- "Failed to fetch settlement status for group:",
- groupId,
- error
- );
+ console.error("Error calculating settlements:", error);
return {
- isSettled: true,
- owesAmount: 0,
- owedAmount: 0,
+ isSettled: false,
netBalance: 0,
+ totalOwed: 0,
+ totalToReceive: 0,
+ settlements: [],
};
}
};
const fetchGroups = async () => {
try {
- setIsLoading(true);
- const response = await getGroups();
- const groupsList = response.data.groups;
- setGroups(groupsList);
-
- // Fetch settlement status for each group
- if (user?._id) {
- const settlementPromises = groupsList.map(async (group) => {
+ const response = await getGroups(); // Remove token parameter
+ console.log('Groups API Response:', response); // Debug log
+
+ // Handle different response structures
+ let groupsData = response.data;
+ if (!groupsData) {
+ groupsData = response; // Sometimes the response itself is the data
+ }
+ if (!Array.isArray(groupsData)) {
+ console.warn('Groups data is not an array:', groupsData);
+ groupsData = []; // Fallback to empty array
+ }
+
+ setGroups(groupsData);
+
+ // Calculate settlement status for each group
+ if (groupsData.length > 0) {
+ const settlementPromises = groupsData.map(async (group) => {
const status = await calculateSettlementStatus(group._id, user._id);
return { groupId: group._id, status };
});
- const settlementResults = await Promise.all(settlementPromises);
- const settlementMap = {};
- settlementResults.forEach(({ groupId, status }) => {
- settlementMap[groupId] = status;
+ const settlementsData = await Promise.all(settlementPromises);
+ const settlementsMap = {};
+ settlementsData.forEach(({ groupId, status }) => {
+ settlementsMap[groupId] = status;
});
- setGroupSettlements(settlementMap);
+ setGroupSettlements(settlementsMap);
+ } else {
+ // No groups, clear settlements
+ setGroupSettlements({});
}
+
+ // Animate balance numbers
+ Animated.timing(balanceAnim, {
+ toValue: 1,
+ duration: 600,
+ useNativeDriver: false,
+ }).start();
+
} catch (error) {
console.error("Failed to fetch groups:", error);
- Alert.alert("Error", "Failed to fetch groups.");
+ Alert.alert("Error", "Failed to fetch groups. Please try again.");
} finally {
setIsLoading(false);
+ setIsRefreshing(false);
}
};
- useEffect(() => {
- if (token) {
- fetchGroups();
- }
- }, [token]);
+ const onRefresh = async () => {
+ setIsRefreshing(true);
+ await fetchGroups();
+ };
+
+ const showModal = async () => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ setModalVisible(true);
+ };
+
+ const hideModal = () => {
+ setModalVisible(false);
+ setNewGroupName("");
+ };
const handleCreateGroup = async () => {
- if (!newGroupName) {
+ if (!newGroupName.trim()) {
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Alert.alert("Error", "Please enter a group name.");
return;
}
+
setIsCreatingGroup(true);
try {
- await createGroup(newGroupName);
+ await createGroup(newGroupName.trim()); // Remove token parameter
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
hideModal();
- setNewGroupName("");
- await fetchGroups(); // Refresh the groups list
+ fetchGroups();
} catch (error) {
console.error("Failed to create group:", error);
- Alert.alert("Error", "Failed to create group.");
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+ Alert.alert("Error", "Failed to create group. Please try again.");
} finally {
setIsCreatingGroup(false);
}
};
- const currencySymbol = getCurrencySymbol();
-
- const renderGroup = ({ item }) => {
- const settlementStatus = groupSettlements[item._id];
+ const calculateOverallBalance = () => {
+ let totalOwed = 0;
+ let totalToReceive = 0;
+
+ Object.values(groupSettlements).forEach(settlement => {
+ totalOwed += settlement.totalOwed || 0;
+ totalToReceive += settlement.totalToReceive || 0;
+ });
- // Generate settlement status text
- const getSettlementStatusText = () => {
- if (!settlementStatus) {
- return "Calculating balances...";
- }
-
- if (settlementStatus.isSettled) {
- return "✓ You are settled up.";
- }
-
- if (settlementStatus.netBalance > 0) {
- return `You are owed ${formatCurrency(settlementStatus.netBalance)}.`;
- } else if (settlementStatus.netBalance < 0) {
- return `You owe ${formatCurrency(
- Math.abs(settlementStatus.netBalance)
- )}.`;
- }
-
- return "You are settled up.";
+ return {
+ net: totalToReceive - totalOwed,
+ totalOwed,
+ totalToReceive,
+ totalGroups: groups.length,
};
+ };
- // Get text color based on settlement status
- const getStatusColor = () => {
- if (!settlementStatus || settlementStatus.isSettled) {
- return "#4CAF50"; // Green for settled
- }
+ const overallBalance = calculateOverallBalance();
- if (settlementStatus.netBalance > 0) {
- return "#4CAF50"; // Green for being owed money
- } else if (settlementStatus.netBalance < 0) {
- return "#F44336"; // Red for owing money
- }
+ const renderQuickActions = () => (
+
+ Quick Actions
+
+ 👥}
+ onPress={showModal}
+ variant="accent"
+ style={styles.quickActionCard}
+ />
+ 🔗}
+ onPress={() => navigation.navigate('JoinGroup')}
+ variant="success"
+ style={styles.quickActionCard}
+ />
+ 👋}
+ onPress={() => navigation.navigate('Friends')}
+ variant="warning"
+ style={styles.quickActionCard}
+ />
+ ⚙️}
+ onPress={() => navigation.navigate('Account')}
+ variant="accent"
+ style={styles.quickActionCard}
+ />
+
+
+ );
- return "#4CAF50"; // Default green
- };
+ const renderGroup = ({ item: group, index }) => {
+ const settlement = groupSettlements[group._id];
+ if (!settlement) return null;
- const isImage =
- item.imageUrl && /^(https?:|data:image)/.test(item.imageUrl);
- const groupIcon = item.imageUrl || item.name?.charAt(0) || "?";
return (
-
- navigation.navigate("GroupDetails", {
- groupId: item._id,
- groupName: item.name,
- groupIcon,
- })
- }
+
-
- isImage ? (
-
- ) : (
-
- )
- }
+ navigation.navigate('GroupDetails', {
+ groupId: group._id,
+ groupName: group.name
+ })}
+ style={styles.groupCard}
/>
-
-
- {getSettlementStatusText()}
-
-
-
+
);
};
+ if (isLoading) {
+ return (
+
+
+
+ Loading your groups...
+
+
+ );
+ }
+
return (
+ {/* Modern Header */}
+ navigation.navigate('Account')}
+ style={styles.profileButton}
+ >
+
+
+ {user?.name?.charAt(0).toUpperCase() || 'U'}
+
+
+
+ }
+ variant="gradient"
+ />
+
+
+ }
+ >
+ {/* Balance Overview Card */}
+
+
+ Your Balance
+
+
+ = 0 ? colors.semantic.success : colors.semantic.error }
+ ]}>
+ {overallBalance.net >= 0 ? '+' : ''}${Math.abs(overallBalance.net).toFixed(2)}
+
+
+ {overallBalance.net >= 0 ? 'You are owed' : 'You owe'}
+
+
+
+
+
+
+ +${overallBalance.totalToReceive.toFixed(2)}
+
+ To receive
+
+
+
+
+ -${overallBalance.totalOwed.toFixed(2)}
+
+ To pay
+
+
+
+
+
+ {/* Quick Actions */}
+
+ {renderQuickActions()}
+
+
+ {/* Groups Section */}
+
+ Your Groups ({groups.length})
+
+ {groups.length === 0 ? (
+
+ 👥
+ No Groups Yet
+
+ Create your first group to start splitting expenses with friends!
+
+
+
+ ) : (
+ item._id}
+ renderItem={renderGroup}
+ scrollEnabled={false}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.groupsList}
+ />
+ )}
+
+
+
+ {/* Floating Action Button */}
+
+
+ {/* Create Group Modal */}
- Create a New Group
-
-
-
-
+
+ Create New Group
+
+ Start splitting expenses with your friends
+
-
-
-
-
- navigation.navigate("JoinGroup", { onGroupJoined: fetchGroups })
- }
- />
-
+
- {isLoading ? (
-
-
-
- ) : (
- item._id}
- contentContainerStyle={styles.list}
- ListEmptyComponent={
-
- No groups found. Create or join one!
-
- }
- onRefresh={fetchGroups}
- refreshing={isLoading}
- />
- )}
+
+
+
+
+
+
+
);
};
@@ -257,39 +471,172 @@ const HomeScreen = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
+ backgroundColor: colors.background.primary,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: colors.background.primary,
+ },
+ loadingGradient: {
+ padding: spacing.xl,
+ borderRadius: borderRadius.lg,
+ alignItems: 'center',
+ },
+ loadingText: {
+ ...typography.body,
+ color: '#FFFFFF',
+ marginTop: spacing.md,
+ },
+ content: {
+ flex: 1,
+ },
+ profileButton: {
+ padding: spacing.xs,
+ },
+ profileAvatar: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ alignItems: 'center',
+ justifyContent: 'center',
},
- loaderContainer: {
+ profileInitial: {
+ ...typography.label,
+ color: '#FFFFFF',
+ },
+ balanceContainer: {
+ margin: spacing.lg,
+ },
+ balanceCard: {
+ padding: spacing.xl,
+ alignItems: 'center',
+ },
+ balanceTitle: {
+ ...typography.label,
+ color: colors.text.secondary,
+ marginBottom: spacing.sm,
+ },
+ balanceAmountContainer: {
+ alignItems: 'center',
+ marginBottom: spacing.lg,
+ },
+ balanceAmount: {
+ ...typography.display,
+ fontSize: 36,
+ marginBottom: spacing.xs,
+ },
+ balanceLabel: {
+ ...typography.caption,
+ color: colors.text.secondary,
+ },
+ balanceBreakdown: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ width: '100%',
+ },
+ balanceItem: {
flex: 1,
- justifyContent: "center",
- alignItems: "center",
+ alignItems: 'center',
+ },
+ balanceItemAmount: {
+ ...typography.h4,
+ color: colors.text.primary,
+ marginBottom: 4,
+ },
+ balanceItemLabel: {
+ ...typography.caption,
+ color: colors.text.secondary,
+ },
+ balanceDivider: {
+ width: 1,
+ height: 40,
+ backgroundColor: colors.border.subtle,
+ marginHorizontal: spacing.lg,
+ },
+ quickActionsContainer: {
+ padding: spacing.lg,
+ },
+ sectionTitle: {
+ ...typography.h3,
+ color: colors.text.primary,
+ marginBottom: spacing.md,
},
- list: {
- padding: 16,
+ quickActionsGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: spacing.md,
},
- card: {
- marginBottom: 16,
+ quickActionCard: {
+ width: (width - spacing.lg * 3) / 2,
},
- settlementStatus: {
- fontWeight: "500",
- marginTop: 4,
+ actionIcon: {
+ fontSize: 24,
},
- emptyText: {
- textAlign: "center",
- marginTop: 20,
+ groupsSection: {
+ padding: spacing.lg,
+ },
+ emptyStateCard: {
+ padding: spacing.xl,
+ alignItems: 'center',
+ },
+ emptyStateIcon: {
+ fontSize: 48,
+ marginBottom: spacing.lg,
+ },
+ emptyStateTitle: {
+ ...typography.h2,
+ color: colors.text.primary,
+ marginBottom: spacing.sm,
+ },
+ emptyStateText: {
+ ...typography.body,
+ color: colors.text.secondary,
+ textAlign: 'center',
+ marginBottom: spacing.lg,
+ },
+ emptyStateButton: {
+ minWidth: 160,
+ },
+ groupsList: {
+ gap: spacing.md,
+ },
+ groupItemContainer: {
+ marginBottom: spacing.sm,
+ },
+ groupCard: {
+ marginBottom: 0,
},
modalContainer: {
- backgroundColor: "white",
- padding: 20,
- margin: 20,
- borderRadius: 8,
+ margin: spacing.lg,
+ justifyContent: 'center',
+ },
+ modalCard: {
+ padding: spacing.xl,
},
modalTitle: {
- fontSize: 20,
- marginBottom: 20,
- textAlign: "center",
+ ...typography.h2,
+ color: colors.text.primary,
+ marginBottom: spacing.sm,
+ textAlign: 'center',
+ },
+ modalSubtitle: {
+ ...typography.body,
+ color: colors.text.secondary,
+ textAlign: 'center',
+ marginBottom: spacing.lg,
},
- input: {
- marginBottom: 20,
+ modalInput: {
+ marginBottom: spacing.lg,
+ },
+ modalActions: {
+ flexDirection: 'row',
+ gap: spacing.md,
+ },
+ modalActionButton: {
+ flex: 1,
},
});
diff --git a/frontend/screens/JoinGroupScreen.js b/frontend/screens/JoinGroupScreen.js
index 153e4eac..693fa513 100644
--- a/frontend/screens/JoinGroupScreen.js
+++ b/frontend/screens/JoinGroupScreen.js
@@ -1,8 +1,11 @@
import { useContext, useState } from "react";
-import { Alert, StyleSheet, View } from "react-native";
-import { Appbar, Button, TextInput, Title } from "react-native-paper";
+import { Alert, ScrollView, StyleSheet, View } from "react-native";
+import { Appbar, Button, Text, TextInput } from "react-native-paper";
import { joinGroup } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
+import { FadeInView, ScaleInView } from "../utils/animations";
+import { GradientCard } from "../utils/gradients";
+import { borderRadius, colors, shadows, spacing, typography } from "../utils/theme";
const JoinGroupScreen = ({ navigation, route }) => {
const { token } = useContext(AuthContext);
@@ -18,7 +21,7 @@ const JoinGroupScreen = ({ navigation, route }) => {
setIsJoining(true);
try {
await joinGroup(joinCode);
- Alert.alert("Success", "Successfully joined the group.");
+ Alert.alert("Success", "Successfully joined the group! 🎉");
onGroupJoined(); // Call the callback to refresh the groups list
navigation.goBack();
} catch (error) {
@@ -34,29 +37,80 @@ const JoinGroupScreen = ({ navigation, route }) => {
return (
-
- navigation.goBack()} />
-
-
-
- Enter Group Code
-
+ navigation.goBack()}
+ iconColor={colors.primary}
+ />
+
-
-
+
+
+
+
+
+ Join a Group! 🚀
+
+ Enter the group code shared by your friends to start splitting expenses together
+
+
+
+
+
+
+ Group Join Code
+
+
+ 💡 Ask your friend for the 6-8 character group code
+
+
+
+
+
+
+
+
+ 💡 How to get a group code?
+
+ • Ask the group creator to share the join code{'\n'}
+ • Look for it in your group chat or invitation message{'\n'}
+ • The code is usually 6-8 characters long
+
+
+
+
);
};
@@ -64,15 +118,97 @@ const JoinGroupScreen = ({ navigation, route }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
+ backgroundColor: colors.background,
+ },
+ header: {
+ backgroundColor: colors.surface,
+ elevation: 0,
+ shadowOpacity: 0,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.outlineVariant,
+ },
+ headerTitle: {
+ ...typography.h3,
+ color: colors.onSurface,
},
content: {
- padding: 16,
+ flex: 1,
+ padding: spacing.lg,
+ },
+ heroCard: {
+ padding: spacing.xl,
+ marginBottom: spacing.xl,
+ alignItems: 'center',
+ },
+ heroTitle: {
+ ...typography.h2,
+ color: 'white',
+ textAlign: 'center',
+ marginBottom: spacing.sm,
+ fontWeight: '700',
+ },
+ heroSubtitle: {
+ ...typography.body1,
+ color: 'rgba(255, 255, 255, 0.9)',
+ textAlign: 'center',
+ lineHeight: 24,
+ },
+ formContainer: {
+ flex: 1,
+ },
+ inputContainer: {
+ marginBottom: spacing.xl,
+ },
+ inputLabel: {
+ ...typography.label,
+ color: colors.onSurface,
+ marginBottom: spacing.sm,
+ fontWeight: '600',
},
input: {
- marginBottom: 16,
+ backgroundColor: colors.surface,
+ fontSize: 18,
+ fontWeight: '600',
+ letterSpacing: 1,
+ textAlign: 'center',
+ textTransform: 'uppercase',
+ },
+ helpText: {
+ ...typography.caption,
+ color: colors.onSurfaceVariant,
+ marginTop: spacing.sm,
+ textAlign: 'center',
+ },
+ joinButton: {
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.lg,
+ marginBottom: spacing.xl,
+ ...shadows.medium,
+ },
+ buttonContent: {
+ paddingVertical: spacing.sm,
+ },
+ buttonLabel: {
+ ...typography.label,
+ fontSize: 16,
+ fontWeight: '700',
+ },
+ tipContainer: {
+ backgroundColor: colors.surfaceVariant,
+ borderRadius: borderRadius.md,
+ padding: spacing.lg,
+ borderLeftWidth: 4,
+ borderLeftColor: colors.secondary,
+ },
+ tipTitle: {
+ ...typography.h4,
+ color: colors.onSurface,
+ marginBottom: spacing.sm,
},
- button: {
- marginTop: 8,
+ tipText: {
+ ...typography.body2,
+ color: colors.onSurfaceVariant,
+ lineHeight: 20,
},
});
diff --git a/frontend/screens/ModernAddExpenseScreen.js b/frontend/screens/ModernAddExpenseScreen.js
new file mode 100644
index 00000000..75dc0f3b
--- /dev/null
+++ b/frontend/screens/ModernAddExpenseScreen.js
@@ -0,0 +1,586 @@
+// Modern Add Expense Screen - Following Blueprint Specifications
+// Implements the core "Add Expense" journey with glassmorphism and Gen Z UX
+
+import * as Haptics from 'expo-haptics';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useContext, useEffect, useRef, useState } from "react";
+import {
+ Alert,
+ Animated,
+ Dimensions,
+ ScrollView,
+ StyleSheet,
+ View
+} from "react-native";
+import { ActivityIndicator, Text } from "react-native-paper";
+import { createExpense, getGroupMembers } from "../api/groups";
+import { AuthContext } from "../context/AuthContext";
+
+// Import our modern components
+import Button from '../components/core/Button';
+import { CurrencyInput, EnhancedTextInput } from '../components/core/Input';
+import { ModernHeader } from '../components/navigation/ModernNavigation';
+import { GlassCard } from '../utils/cards';
+import { borderRadius, colors, spacing, typography } from '../utils/theme';
+
+const { width: screenWidth } = Dimensions.get('window');
+
+const ModernAddExpenseScreen = ({ route, navigation }) => {
+ const { groupId } = route.params;
+ const { token, user } = useContext(AuthContext);
+
+ // Form state
+ const [description, setDescription] = useState("");
+ const [amount, setAmount] = useState("");
+ const [members, setMembers] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [splitMethod, setSplitMethod] = useState("equal");
+ const [payerId, setPayerId] = useState(null);
+
+ // Split method states
+ const [percentages, setPercentages] = useState({});
+ const [shares, setShares] = useState({});
+ const [exactAmounts, setExactAmounts] = useState({});
+ const [selectedMembers, setSelectedMembers] = useState({});
+
+ // Animation refs
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(50)).current;
+ const progressAnim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ // Initial animation
+ Animated.parallel([
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 300,
+ useNativeDriver: true,
+ }),
+ Animated.timing(slideAnim, {
+ toValue: 0,
+ duration: 300,
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ const fetchMembers = async () => {
+ try {
+ const response = await getGroupMembers(groupId);
+ setMembers(response.data);
+
+ // Initialize split states
+ const initialShares = {};
+ const initialPercentages = {};
+ const initialExactAmounts = {};
+ const initialSelectedMembers = {};
+ const numMembers = response.data.length;
+
+ // Calculate percentages using integer math to avoid floating-point errors
+ const basePercentage = Math.floor(100 / numMembers);
+ const remainder = 100 - basePercentage * numMembers;
+
+ response.data.forEach((member, index) => {
+ initialShares[member.userId] = "1";
+
+ // Distribute percentages using integer math
+ let memberPercentage = basePercentage;
+ if (index < remainder) {
+ memberPercentage += 1;
+ }
+ initialPercentages[member.userId] = memberPercentage.toString();
+ initialExactAmounts[member.userId] = "0.00";
+ initialSelectedMembers[member.userId] = true;
+ });
+
+ setShares(initialShares);
+ setPercentages(initialPercentages);
+ setExactAmounts(initialExactAmounts);
+ setSelectedMembers(initialSelectedMembers);
+
+ // Set default payer to current user if they're a member
+ const currentUserMember = response.data.find(
+ (member) => member.userId === user._id
+ );
+ if (currentUserMember) {
+ setPayerId(user._id);
+ } else if (response.data.length > 0) {
+ setPayerId(response.data[0].userId);
+ }
+ } catch (error) {
+ console.error("Failed to fetch members:", error);
+ Alert.alert("Error", "Failed to fetch group members.");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (token && groupId) {
+ fetchMembers();
+ }
+ }, [token, groupId]);
+
+ const handleAddExpense = async () => {
+ if (!description || !amount) {
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+ Alert.alert("Missing Information", "Please fill in the expense description and amount.");
+ return;
+ }
+
+ if (!payerId) {
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+ Alert.alert("Missing Payer", "Please select who paid for this expense.");
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ // Progress animation
+ Animated.timing(progressAnim, {
+ toValue: 1,
+ duration: 2000,
+ useNativeDriver: false,
+ }).start();
+
+ try {
+ const expenseData = {
+ description,
+ amount: parseFloat(amount),
+ paidBy: payerId,
+ splitMethod,
+ ...(splitMethod === "equal" && { selectedMembers }),
+ ...(splitMethod === "percentage" && { percentages }),
+ ...(splitMethod === "shares" && { shares }),
+ ...(splitMethod === "exact" && { exactAmounts }),
+ };
+
+ await createExpense(groupId, expenseData, token);
+
+ // Success haptic feedback
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
+
+ // Success animation and navigation
+ Animated.parallel([
+ Animated.timing(fadeAnim, {
+ toValue: 0,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ Animated.timing(slideAnim, {
+ toValue: -50,
+ duration: 200,
+ useNativeDriver: true,
+ }),
+ ]).start(() => {
+ navigation.goBack();
+ });
+
+ } catch (error) {
+ console.error("Failed to create expense:", error);
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+ Alert.alert("Error", "Failed to create expense. Please try again.");
+
+ // Reset progress animation
+ progressAnim.setValue(0);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const splitMethods = [
+ { value: "equal", label: "Split Equally", icon: "⚖️" },
+ { value: "percentage", label: "By Percentage", icon: "📊" },
+ { value: "shares", label: "By Shares", icon: "🔢" },
+ { value: "exact", label: "Exact Amounts", icon: "💰" },
+ ];
+
+ const handleSplitMethodChange = async (method) => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ setSplitMethod(method);
+ };
+
+ const handlePayerSelect = async (memberId) => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ setPayerId(memberId);
+ };
+
+ const toggleMemberSelection = async (memberId) => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ setSelectedMembers(prev => ({
+ ...prev,
+ [memberId]: !prev[memberId]
+ }));
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ Loading group members...
+
+
+ );
+ }
+
+ return (
+
+ {/* Modern Header */}
+
+
+
+
+ {/* Expense Details Card */}
+
+ 💰 Expense Details
+
+
+
+
+
+
+ {/* Payer Selection Card */}
+
+ 👤 Who Paid?
+
+
+ {members.map((member) => (
+ handlePayerSelect(member.userId)}
+ style={[
+ styles.payerOption,
+ payerId === member.userId && styles.payerOptionSelected,
+ ]}
+ >
+
+
+ {member.name.charAt(0).toUpperCase()}
+
+
+
+ {member.name}
+
+
+ ))}
+
+
+
+ {/* Split Method Card */}
+
+ 📝 How to Split?
+
+
+ {splitMethods.map((method) => (
+ handleSplitMethodChange(method.value)}
+ style={[
+ styles.splitMethodOption,
+ splitMethod === method.value && styles.splitMethodSelected,
+ ]}
+ >
+ {method.icon}
+
+ {method.label}
+
+
+ ))}
+
+
+
+ {/* Member Selection for Equal Split */}
+ {splitMethod === "equal" && (
+
+ 👥 Who's Involved?
+
+
+ {members.map((member) => (
+ toggleMemberSelection(member.userId)}
+ style={styles.memberOption}
+ >
+
+
+
+ {member.name.charAt(0).toUpperCase()}
+
+
+ {member.name}
+
+
+ {selectedMembers[member.userId] && (
+ ✓
+ )}
+
+
+ ))}
+
+
+ )}
+
+ {/* Submit Button */}
+
+
+
+
+
+
+ {/* Progress Bar */}
+ {isSubmitting && (
+
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background.primary,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingGradient: {
+ padding: spacing.xl,
+ borderRadius: borderRadius.lg,
+ alignItems: 'center',
+ },
+ loadingText: {
+ ...typography.body,
+ color: '#FFFFFF',
+ marginTop: spacing.md,
+ },
+ content: {
+ flex: 1,
+ },
+ scrollContent: {
+ padding: spacing.lg,
+ paddingBottom: spacing.xl * 2,
+ },
+ sectionCard: {
+ marginBottom: spacing.lg,
+ },
+ sectionTitle: {
+ ...typography.h3,
+ color: colors.text.primary,
+ marginBottom: spacing.md,
+ },
+ input: {
+ marginBottom: spacing.md,
+ },
+ payerGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: spacing.md,
+ },
+ payerOption: {
+ alignItems: 'center',
+ padding: spacing.md,
+ borderRadius: borderRadius.md,
+ minWidth: 80,
+ },
+ payerOptionSelected: {
+ backgroundColor: `${colors.brand.accent}15`,
+ },
+ payerAvatar: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ backgroundColor: colors.background.secondary,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: spacing.sm,
+ borderWidth: 2,
+ borderColor: 'transparent',
+ },
+ payerAvatarSelected: {
+ borderColor: colors.brand.accent,
+ backgroundColor: `${colors.brand.accent}20`,
+ },
+ payerInitial: {
+ ...typography.label,
+ color: colors.text.primary,
+ },
+ payerName: {
+ ...typography.caption,
+ color: colors.text.secondary,
+ textAlign: 'center',
+ },
+ payerNameSelected: {
+ color: colors.brand.accent,
+ fontWeight: '600',
+ },
+ splitMethodGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: spacing.sm,
+ },
+ splitMethodOption: {
+ flex: 1,
+ minWidth: '45%',
+ padding: spacing.md,
+ borderRadius: borderRadius.md,
+ alignItems: 'center',
+ backgroundColor: colors.background.secondary,
+ borderWidth: 2,
+ borderColor: 'transparent',
+ },
+ splitMethodSelected: {
+ borderColor: colors.brand.accent,
+ backgroundColor: `${colors.brand.accent}10`,
+ },
+ splitMethodIcon: {
+ fontSize: 24,
+ marginBottom: spacing.sm,
+ },
+ splitMethodLabel: {
+ ...typography.caption,
+ color: colors.text.secondary,
+ textAlign: 'center',
+ },
+ splitMethodLabelSelected: {
+ color: colors.brand.accent,
+ fontWeight: '600',
+ },
+ membersList: {
+ gap: spacing.sm,
+ },
+ memberOption: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: spacing.md,
+ borderRadius: borderRadius.md,
+ backgroundColor: colors.background.secondary,
+ },
+ memberInfo: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ memberAvatar: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: colors.glass.background,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: spacing.md,
+ borderWidth: 2,
+ borderColor: 'transparent',
+ },
+ memberAvatarSelected: {
+ borderColor: colors.brand.accent,
+ backgroundColor: `${colors.brand.accent}20`,
+ },
+ memberInitial: {
+ ...typography.label,
+ color: colors.text.primary,
+ },
+ memberName: {
+ ...typography.body,
+ color: colors.text.primary,
+ },
+ checkbox: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ borderWidth: 2,
+ borderColor: colors.border.subtle,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ checkboxSelected: {
+ borderColor: colors.brand.accent,
+ backgroundColor: colors.brand.accent,
+ },
+ checkmark: {
+ color: '#FFFFFF',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ submitContainer: {
+ marginTop: spacing.lg,
+ },
+ submitButton: {
+ minHeight: 56,
+ },
+ progressBar: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ height: 3,
+ backgroundColor: colors.brand.accent,
+ },
+});
+
+export default ModernAddExpenseScreen;
diff --git a/frontend/screens/ModernGroupDetailsScreen.js b/frontend/screens/ModernGroupDetailsScreen.js
new file mode 100644
index 00000000..587ff07a
--- /dev/null
+++ b/frontend/screens/ModernGroupDetailsScreen.js
@@ -0,0 +1,455 @@
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import * as Haptics from 'expo-haptics';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useContext, useEffect, useState } from 'react';
+import {
+ Alert,
+ Animated,
+ Dimensions,
+ StyleSheet,
+ Text,
+ View
+} from 'react-native';
+import {
+ getGroupExpenses,
+ getGroupMembers,
+ getOptimizedSettlements
+} from '../api/groups';
+import { ModernButton } from '../components/core/Button';
+import { FloatingActionButton, ModernHeader } from '../components/navigation/ModernNavigation';
+import { AuthContext } from '../context/AuthContext';
+import { ExpenseCard, GlassCard } from '../utils/cards';
+import { theme } from '../utils/theme';
+
+const { width, height } = Dimensions.get('window');
+
+const ModernGroupDetailsScreen = ({ route, navigation }) => {
+ const { groupId, groupName } = route.params;
+ const { token, user } = useContext(AuthContext);
+ const [members, setMembers] = useState([]);
+ const [expenses, setExpenses] = useState([]);
+ const [settlements, setSettlements] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
+
+ // Animated values
+ const scrollY = new Animated.Value(0);
+ const fadeAnim = new Animated.Value(0);
+ const scaleAnim = new Animated.Value(0.9);
+
+ const formatCurrency = (amount) => `₹${amount.toFixed(2)}`;
+
+ const fetchData = async (showRefreshing = false) => {
+ try {
+ if (showRefreshing) setRefreshing(true);
+ else setIsLoading(true);
+
+ const [membersResponse, expensesResponse, settlementsResponse] =
+ await Promise.all([
+ getGroupMembers(groupId),
+ getGroupExpenses(groupId),
+ getOptimizedSettlements(groupId),
+ ]);
+
+ setMembers(membersResponse.data);
+ setExpenses(expensesResponse.data.expenses || []);
+ setSettlements(settlementsResponse.data.optimizedSettlements || []);
+
+ // Animate in
+ Animated.parallel([
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 300,
+ useNativeDriver: true,
+ }),
+ Animated.spring(scaleAnim, {
+ toValue: 1,
+ tension: 50,
+ friction: 7,
+ useNativeDriver: true,
+ }),
+ ]).start();
+
+ } catch (error) {
+ console.error('Failed to fetch group details:', error);
+ Alert.alert('Error', 'Failed to fetch group details.');
+ } finally {
+ setIsLoading(false);
+ setRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ if (token && groupId) {
+ fetchData();
+ }
+ }, [token, groupId]);
+
+ const getMemberName = (userId) => {
+ const member = members.find((m) => m.userId === userId);
+ return member ? member.user.name : 'Unknown';
+ };
+
+ const calculateUserBalance = () => {
+ let totalOwed = 0;
+ let totalToReceive = 0;
+
+ expenses.forEach(expense => {
+ const userSplit = expense.splits.find(s => s.userId === user._id);
+ const userShare = userSplit ? userSplit.amount : 0;
+ const paidByMe = (expense.paidBy || expense.createdBy) === user._id;
+ const net = paidByMe ? expense.amount - userShare : -userShare;
+
+ if (net > 0) totalToReceive += net;
+ else if (net < 0) totalOwed += Math.abs(net);
+ });
+
+ return { totalOwed, totalToReceive };
+ };
+
+ const renderBalanceOverview = () => {
+ const { totalOwed, totalToReceive } = calculateUserBalance();
+ const netBalance = totalToReceive - totalOwed;
+
+ return (
+
+
+
+ Your Balance
+ = 0 ? '#10B981' : '#EF4444' }
+ ]}>
+ {netBalance >= 0 ? '+' : ''}{formatCurrency(netBalance)}
+
+
+ {netBalance > 0
+ ? "You're owed money"
+ : netBalance < 0
+ ? "You owe money"
+ : "You're all settled up!"
+ }
+
+
+
+
+ {/* Quick stats */}
+
+
+
+ {formatCurrency(totalToReceive)}
+ You'll receive
+
+
+
+
+ {formatCurrency(totalOwed)}
+ You owe
+
+
+
+ );
+ };
+
+ const renderExpenseItem = (expense, index) => {
+ const userSplit = expense.splits.find(s => s.userId === user._id);
+ const userShare = userSplit ? userSplit.amount : 0;
+ const paidByMe = (expense.paidBy || expense.createdBy) === user._id;
+ const net = paidByMe ? expense.amount - userShare : -userShare;
+
+ return (
+
+ {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ // Navigate to expense details if needed
+ }}
+ />
+
+ );
+ };
+
+ const handleAddExpense = () => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ navigation.navigate('AddExpense', { groupId });
+ };
+
+ const handleRefresh = () => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ fetchData(true);
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+ Loading group details...
+
+
+
+ );
+ }
+
+ return (
+
+
+ navigation.goBack()}
+ rightAction={{
+ icon: 'cog',
+ onPress: () => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ navigation.navigate('GroupSettings', { groupId });
+ }
+ }}
+ />
+
+
+ {renderBalanceOverview()}
+
+ {/* Expenses Section */}
+
+
+ Recent Expenses
+
+ {expenses.length} expense{expenses.length !== 1 ? 's' : ''}
+
+
+
+ {expenses.length === 0 ? (
+
+
+ No expenses yet
+
+ Add your first expense to start tracking group spending
+
+
+
+ ) : (
+
+ {expenses.map((expense, index) => renderExpenseItem(expense, index))}
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ gradient: {
+ flex: 1,
+ },
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ loadingContent: {
+ alignItems: 'center',
+ },
+ loadingText: {
+ fontSize: 16,
+ color: theme.colors.text.secondary,
+ fontFamily: theme.typography.medium,
+ },
+ scrollView: {
+ flex: 1,
+ },
+ scrollContent: {
+ padding: theme.spacing.lg,
+ paddingBottom: 100,
+ },
+ balanceContainer: {
+ marginBottom: theme.spacing.xl,
+ },
+ balanceGradient: {
+ borderRadius: theme.borderRadius.xl,
+ padding: theme.spacing.xl,
+ marginBottom: theme.spacing.lg,
+ },
+ balanceContent: {
+ alignItems: 'center',
+ },
+ balanceLabel: {
+ fontSize: 14,
+ color: 'rgba(255, 255, 255, 0.8)',
+ fontFamily: theme.typography.medium,
+ marginBottom: theme.spacing.xs,
+ },
+ balanceAmount: {
+ fontSize: 32,
+ fontFamily: theme.typography.bold,
+ marginBottom: theme.spacing.xs,
+ },
+ balanceSubtext: {
+ fontSize: 14,
+ color: 'rgba(255, 255, 255, 0.7)',
+ fontFamily: theme.typography.regular,
+ },
+ quickStats: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ gap: theme.spacing.md,
+ },
+ statCard: {
+ flex: 1,
+ padding: theme.spacing.lg,
+ alignItems: 'center',
+ },
+ statValue: {
+ fontSize: 18,
+ fontFamily: theme.typography.bold,
+ color: theme.colors.text.primary,
+ marginTop: theme.spacing.xs,
+ },
+ statLabel: {
+ fontSize: 12,
+ color: theme.colors.text.secondary,
+ fontFamily: theme.typography.medium,
+ marginTop: theme.spacing.xs,
+ },
+ expensesSection: {
+ marginBottom: theme.spacing.xl,
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: theme.spacing.lg,
+ },
+ sectionTitle: {
+ fontSize: 20,
+ fontFamily: theme.typography.bold,
+ color: theme.colors.text.primary,
+ },
+ sectionSubtitle: {
+ fontSize: 14,
+ color: theme.colors.text.secondary,
+ fontFamily: theme.typography.medium,
+ },
+ expensesList: {
+ gap: theme.spacing.md,
+ },
+ expenseItem: {
+ marginBottom: theme.spacing.sm,
+ },
+ emptyCard: {
+ padding: theme.spacing.xl,
+ alignItems: 'center',
+ },
+ emptyTitle: {
+ fontSize: 18,
+ fontFamily: theme.typography.bold,
+ color: theme.colors.text.primary,
+ marginBottom: theme.spacing.xs,
+ },
+ emptyText: {
+ fontSize: 14,
+ color: theme.colors.text.secondary,
+ fontFamily: theme.typography.regular,
+ textAlign: 'center',
+ lineHeight: 20,
+ },
+ fab: {
+ position: 'absolute',
+ bottom: theme.spacing.xl,
+ right: theme.spacing.xl,
+ },
+});
+
+export default ModernGroupDetailsScreen;
diff --git a/frontend/utils/animations.js b/frontend/utils/animations.js
new file mode 100644
index 00000000..f26ebdd6
--- /dev/null
+++ b/frontend/utils/animations.js
@@ -0,0 +1,201 @@
+import { useEffect, useRef } from 'react';
+import { Animated, TouchableWithoutFeedback, View } from 'react-native';
+import { animations, colors } from './theme';
+
+// Animated TouchableCard component with press animation
+export const AnimatedCard = ({ children, onPress, style, disabled, ...props }) => {
+ const scaleValue = useRef(new Animated.Value(1)).current;
+ const opacityValue = useRef(new Animated.Value(1)).current;
+
+ const handlePressIn = () => {
+ if (disabled) return;
+
+ Animated.parallel([
+ Animated.timing(scaleValue, {
+ toValue: 0.98,
+ duration: animations.timing.short,
+ useNativeDriver: true,
+ }),
+ Animated.timing(opacityValue, {
+ toValue: 0.8,
+ duration: animations.timing.short,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ };
+
+ const handlePressOut = () => {
+ if (disabled) return;
+
+ Animated.parallel([
+ Animated.timing(scaleValue, {
+ toValue: 1,
+ duration: animations.timing.short,
+ useNativeDriver: true,
+ }),
+ Animated.timing(opacityValue, {
+ toValue: 1,
+ duration: animations.timing.short,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ };
+
+ const animatedStyle = {
+ transform: [{ scale: scaleValue }],
+ opacity: opacityValue,
+ };
+
+ if (onPress && !disabled) {
+ return (
+
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Slide in animation for list items
+export const SlideInView = ({ children, delay = 0, style }) => {
+ const translateY = useRef(new Animated.Value(20)).current;
+ const opacity = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ Animated.parallel([
+ Animated.timing(translateY, {
+ toValue: 0,
+ duration: animations.timing.medium,
+ useNativeDriver: true,
+ }),
+ Animated.timing(opacity, {
+ toValue: 1,
+ duration: animations.timing.medium,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ }, delay);
+
+ return () => clearTimeout(timer);
+ }, [delay, translateY, opacity]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Fade in animation
+export const FadeInView = ({ children, delay = 0, style, duration = animations.timing.medium }) => {
+ const opacity = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ Animated.timing(opacity, {
+ toValue: 1,
+ duration,
+ useNativeDriver: true,
+ }).start();
+ }, delay);
+
+ return () => clearTimeout(timer);
+ }, [delay, opacity, duration]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Scale in animation for FAB and buttons
+export const ScaleInView = ({ children, delay = 0, style }) => {
+ const scale = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ Animated.spring(scale, {
+ toValue: 1,
+ tension: 100,
+ friction: 8,
+ useNativeDriver: true,
+ }).start();
+ }, delay);
+
+ return () => clearTimeout(timer);
+ }, [delay, scale]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Pulse animation for important elements
+export const PulseView = ({ children, style, pulseColor = colors.primary }) => {
+ const pulse = useRef(new Animated.Value(1)).current;
+
+ useEffect(() => {
+ const pulseAnimation = Animated.loop(
+ Animated.sequence([
+ Animated.timing(pulse, {
+ toValue: 1.05,
+ duration: 1000,
+ useNativeDriver: true,
+ }),
+ Animated.timing(pulse, {
+ toValue: 1,
+ duration: 1000,
+ useNativeDriver: true,
+ }),
+ ])
+ );
+
+ pulseAnimation.start();
+
+ return () => pulseAnimation.stop();
+ }, [pulse]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend/utils/cards.js b/frontend/utils/cards.js
new file mode 100644
index 00000000..15da5613
--- /dev/null
+++ b/frontend/utils/cards.js
@@ -0,0 +1,587 @@
+// Advanced Card Components - Following Blueprint Specifications
+// Implements strategic glassmorphism and micro-animations for Gen Z engagement
+
+import * as Haptics from 'expo-haptics';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useRef } from 'react';
+import {
+ Animated,
+ Dimensions,
+ Text,
+ TouchableOpacity,
+ View
+} from 'react-native';
+import theme, {
+ borderRadius,
+ colors,
+ shadows,
+ spacing,
+ typography
+} from './theme';
+
+const { width: screenWidth } = Dimensions.get('window');
+
+// Base Card Component with Glassmorphism
+const GlassCard = ({
+ children,
+ variant = 'standard', // standard, elevated, outlined, glass
+ onPress,
+ style,
+ glassEffect = false,
+ ...props
+}) => {
+ const scaleValue = useRef(new Animated.Value(1)).current;
+
+ const handlePressIn = () => {
+ Animated.spring(scaleValue, {
+ toValue: 0.98,
+ useNativeDriver: true,
+ tension: 300,
+ friction: 10,
+ }).start();
+ };
+
+ const handlePressOut = () => {
+ Animated.spring(scaleValue, {
+ toValue: 1,
+ useNativeDriver: true,
+ tension: 300,
+ friction: 10,
+ }).start();
+ };
+
+ const handlePress = async () => {
+ if (onPress) {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ onPress();
+ }
+ };
+
+ // Variant configurations
+ const getVariantStyle = () => {
+ switch (variant) {
+ case 'elevated':
+ return {
+ backgroundColor: colors.background.secondary,
+ ...shadows.medium,
+ };
+ case 'outlined':
+ return {
+ backgroundColor: 'transparent',
+ borderWidth: 1,
+ borderColor: colors.border.subtle,
+ };
+ case 'glass':
+ return {
+ backgroundColor: colors.glass.background,
+ borderWidth: 1,
+ borderColor: colors.glass.border,
+ // Note: Blur effect would need additional native module
+ };
+ default: // standard
+ return {
+ backgroundColor: colors.background.secondary,
+ ...shadows.small,
+ };
+ }
+ };
+
+ const cardStyle = {
+ borderRadius: borderRadius.lg,
+ padding: spacing.lg,
+ ...getVariantStyle(),
+ ...style,
+ };
+
+ if (onPress) {
+ return (
+
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Expense Card Component for financial data display
+const ExpenseCard = ({
+ title,
+ amount,
+ date,
+ paidBy,
+ category,
+ participants,
+ userShare,
+ isPaidByUser = false,
+ status = 'pending', // pending, settled, owes
+ onPress,
+ style,
+}) => {
+ // Status configuration
+ const statusConfig = {
+ pending: {
+ color: colors.brand.accent,
+ backgroundColor: `${colors.brand.accent}15`,
+ label: 'Pending',
+ },
+ settled: {
+ color: colors.semantic.success,
+ backgroundColor: `${colors.semantic.success}15`,
+ label: 'Settled',
+ },
+ owes: {
+ color: colors.semantic.warning,
+ backgroundColor: `${colors.semantic.warning}15`,
+ label: 'You owe',
+ },
+ };
+
+ const currentStatus = statusConfig[status];
+
+ // Calculate user's net position
+ const netAmount = userShare !== undefined
+ ? (isPaidByUser ? amount - userShare : -userShare)
+ : 0;
+ const isOwed = netAmount > 0;
+ const owesAmount = netAmount < 0;
+
+ const formatCurrency = (amount) => `₹${Math.abs(amount).toFixed(2)}`;
+ const formatDate = (date) => {
+ if (!date) return '';
+ const now = new Date();
+ const expenseDate = new Date(date);
+ const diffTime = Math.abs(now - expenseDate);
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+
+ if (diffDays === 1) return 'Today';
+ if (diffDays === 2) return 'Yesterday';
+ if (diffDays <= 7) return `${diffDays - 1} days ago`;
+ return expenseDate.toLocaleDateString();
+ };
+
+ return (
+
+ {/* Header with title and amount */}
+
+
+ {title}
+
+
+
+ ₹{amount.toFixed(2)}
+
+
+
+ {/* User's position */}
+ {netAmount !== 0 && (
+
+
+ {isOwed
+ ? `💰 You're owed ${formatCurrency(netAmount)}`
+ : `💳 You owe ${formatCurrency(netAmount)}`
+ }
+
+
+ )}
+
+ {/* Details */}
+
+
+
+ Paid by {paidBy} • {formatDate(date)}
+
+ {category && (
+
+ {category}
+
+ )}
+
+
+ {participants && participants.length > 0 && (
+
+
+ {participants.length} people
+
+
+
+ )}
+
+ {/* Status badge */}
+
+
+ {currentStatus.label}
+
+
+
+
+ );
+};
+
+// Group Summary Card with gradient background
+const GroupSummaryCard = ({
+ groupName,
+ totalExpenses,
+ yourBalance,
+ memberCount,
+ onPress,
+ style,
+}) => {
+ const isPositive = yourBalance >= 0;
+ const balanceColor = isPositive ? colors.semantic.success : colors.semantic.error;
+ const balanceLabel = isPositive ? 'You are owed' : 'You owe';
+
+ return (
+
+
+ {/* Group Name */}
+
+ {groupName}
+
+
+ {/* Stats Row */}
+
+
+
+ Total expenses
+
+
+ ${totalExpenses}
+
+
+
+
+
+ {memberCount} members
+
+
+ {[...Array(Math.min(memberCount, 4))].map((_, i) => (
+ 0 ? 4 : 0,
+ }}
+ />
+ ))}
+ {memberCount > 4 && (
+
+ +{memberCount - 4}
+
+ )}
+
+
+
+
+ {/* Balance Section */}
+
+
+ {balanceLabel}
+
+
+ ${Math.abs(yourBalance).toFixed(2)}
+
+
+
+
+ );
+};
+
+// Quick Action Card for common actions
+const QuickActionCard = ({
+ title,
+ subtitle,
+ icon,
+ onPress,
+ variant = 'accent', // accent, success, warning
+ style,
+}) => {
+ const variantConfig = {
+ accent: {
+ backgroundColor: `${colors.brand.accent}10`,
+ borderColor: `${colors.brand.accent}30`,
+ iconColor: colors.brand.accent,
+ },
+ success: {
+ backgroundColor: `${colors.semantic.success}10`,
+ borderColor: `${colors.semantic.success}30`,
+ iconColor: colors.semantic.success,
+ },
+ warning: {
+ backgroundColor: `${colors.semantic.warning}10`,
+ borderColor: `${colors.semantic.warning}30`,
+ iconColor: colors.semantic.warning,
+ },
+ };
+
+ const config = variantConfig[variant];
+
+ return (
+
+ {/* Icon */}
+
+ {icon}
+
+
+ {/* Title */}
+
+ {title}
+
+
+ {/* Subtitle */}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ );
+};
+
+// Legacy Group Card for backward compatibility
+const GroupCard = ({
+ group,
+ onPress,
+ settlementStatus,
+ memberCount,
+ style,
+}) => {
+ const handlePress = async () => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ onPress?.();
+ };
+
+ const getStatusInfo = () => {
+ if (!settlementStatus) {
+ return {
+ text: "Calculating...",
+ type: 'info',
+ icon: '⏳'
+ };
+ }
+
+ if (settlementStatus.isSettled) {
+ return {
+ text: "✨ All settled up!",
+ type: 'settled',
+ icon: '✅'
+ };
+ }
+
+ if (settlementStatus.netBalance > 0) {
+ return {
+ text: `💰 You're owed ${settlementStatus.netBalance}`,
+ type: 'success',
+ icon: '💰'
+ };
+ } else if (settlementStatus.netBalance < 0) {
+ return {
+ text: `💳 You owe ${Math.abs(settlementStatus.netBalance)}`,
+ type: 'warning',
+ icon: '💳'
+ };
+ }
+
+ return {
+ text: "All good! 👍",
+ type: 'info',
+ icon: '👍'
+ };
+ };
+
+ const statusInfo = getStatusInfo();
+
+ return (
+
+
+ {group.name}
+
+
+
+ {memberCount} members
+
+
+
+ {statusInfo.text}
+
+
+ );
+};
+
+export {
+ ExpenseCard, GlassCard, GroupCard // For backward compatibility
+ ,
+ GroupSummaryCard,
+ QuickActionCard
+};
+
diff --git a/frontend/utils/emptyStates.js b/frontend/utils/emptyStates.js
new file mode 100644
index 00000000..3479565b
--- /dev/null
+++ b/frontend/utils/emptyStates.js
@@ -0,0 +1,151 @@
+import { StyleSheet, View } from 'react-native';
+import { Button, Text } from 'react-native-paper';
+import { FadeInView } from './animations';
+import { borderRadius, colors, spacing, typography } from './theme';
+
+// Empty state component for different scenarios
+export const EmptyState = ({
+ icon,
+ title,
+ subtitle,
+ actionText,
+ onAction,
+ illustration
+}) => (
+
+ {illustration && (
+
+ {illustration}
+
+ )}
+
+ {icon && (
+ {icon}
+ )}
+
+ {title}
+
+ {subtitle && (
+ {subtitle}
+ )}
+
+ {actionText && onAction && (
+
+ )}
+
+);
+
+// Specific empty states for different screens
+export const EmptyGroups = ({ onCreateGroup }) => (
+
+);
+
+export const EmptyExpenses = ({ onAddExpense, groupName }) => (
+
+);
+
+export const EmptyFriends = ({ onAddFriend }) => (
+
+);
+
+export const EmptySearch = ({ searchTerm }) => (
+
+);
+
+export const AllSettled = () => (
+
+ ✨
+ All settled up!
+
+ Great job! Everyone in this group is even.
+
+
+);
+
+const styles = StyleSheet.create({
+ emptyContainer: {
+ alignItems: 'center',
+ paddingVertical: spacing.xxl * 2,
+ paddingHorizontal: spacing.lg,
+ },
+ illustrationContainer: {
+ marginBottom: spacing.xl,
+ },
+ emptyIcon: {
+ fontSize: 64,
+ marginBottom: spacing.lg,
+ },
+ emptyTitle: {
+ ...typography.h2,
+ color: colors.onSurface,
+ textAlign: 'center',
+ marginBottom: spacing.sm,
+ },
+ emptySubtitle: {
+ ...typography.body1,
+ color: colors.onSurfaceVariant,
+ textAlign: 'center',
+ lineHeight: 24,
+ marginBottom: spacing.xl,
+ maxWidth: 280,
+ },
+ emptyAction: {
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.lg,
+ },
+ emptyActionContent: {
+ paddingVertical: spacing.sm,
+ paddingHorizontal: spacing.lg,
+ },
+ settledContainer: {
+ alignItems: 'center',
+ paddingVertical: spacing.xl,
+ backgroundColor: colors.successLight,
+ borderRadius: borderRadius.lg,
+ marginVertical: spacing.md,
+ },
+ settledIcon: {
+ fontSize: 48,
+ marginBottom: spacing.md,
+ },
+ settledTitle: {
+ ...typography.h3,
+ color: colors.successDark,
+ fontWeight: '700',
+ marginBottom: spacing.xs,
+ },
+ settledSubtitle: {
+ ...typography.body1,
+ color: colors.successDark,
+ textAlign: 'center',
+ },
+});
diff --git a/frontend/utils/gradients.js b/frontend/utils/gradients.js
new file mode 100644
index 00000000..71b3d92b
--- /dev/null
+++ b/frontend/utils/gradients.js
@@ -0,0 +1,124 @@
+import { LinearGradient } from 'expo-linear-gradient';
+import { StyleSheet } from 'react-native';
+import { borderRadius, colors } from './theme';
+
+// Gradient background component
+export const GradientBackground = ({
+ children,
+ colors: gradientColors = colors.gradientPrimary,
+ style,
+ start = { x: 0, y: 0 },
+ end = { x: 1, y: 1 },
+ ...props
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+// Gradient card component
+export const GradientCard = ({
+ children,
+ colors: gradientColors = colors.gradientPrimary,
+ style,
+ radius = borderRadius.lg,
+ ...props
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+// Gradient text background
+export const GradientTextBackground = ({
+ children,
+ colors: gradientColors = colors.gradientPrimary,
+ style,
+ ...props
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+// Status indicator gradients
+export const StatusGradient = ({ status, children, style, ...props }) => {
+ let gradientColors;
+
+ switch (status) {
+ case 'success':
+ case 'settled':
+ gradientColors = colors.gradientSuccess;
+ break;
+ case 'warning':
+ case 'owed':
+ gradientColors = colors.gradientWarning;
+ break;
+ case 'error':
+ case 'debt':
+ gradientColors = colors.gradientError;
+ break;
+ case 'info':
+ case 'neutral':
+ gradientColors = colors.gradientSecondary;
+ break;
+ default:
+ gradientColors = colors.gradientPrimary;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ gradientContainer: {
+ flex: 1,
+ },
+ gradientCard: {
+ padding: 16,
+ borderRadius: borderRadius.lg,
+ },
+ gradientText: {
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: borderRadius.sm,
+ },
+});
+
+export default {
+ GradientBackground,
+ GradientCard,
+ GradientTextBackground,
+ StatusGradient,
+};
diff --git a/frontend/utils/haptics.js b/frontend/utils/haptics.js
new file mode 100644
index 00000000..97546d0f
--- /dev/null
+++ b/frontend/utils/haptics.js
@@ -0,0 +1,41 @@
+import * as Haptics from 'expo-haptics';
+
+// Haptic feedback utilities for different interaction types
+export const hapticFeedback = {
+ // Light tap for button presses
+ light: () => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ },
+
+ // Medium impact for card selections
+ medium: () => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+ },
+
+ // Heavy impact for important actions
+ heavy: () => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
+ },
+
+ // Success feedback
+ success: () => {
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
+ },
+
+ // Warning feedback
+ warning: () => {
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
+ },
+
+ // Error feedback
+ error: () => {
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+ },
+
+ // Selection feedback
+ selection: () => {
+ Haptics.selectionAsync();
+ },
+};
+
+export default hapticFeedback;
diff --git a/frontend/utils/icons.js b/frontend/utils/icons.js
new file mode 100644
index 00000000..2b81ad85
--- /dev/null
+++ b/frontend/utils/icons.js
@@ -0,0 +1,222 @@
+import { StyleSheet } from 'react-native';
+import { IconButton } from 'react-native-paper';
+import { borderRadius, colors } from './theme';
+
+// Consistent icon button with theme support
+export const ThemedIconButton = ({
+ icon,
+ size = 24,
+ onPress,
+ variant = 'default', // default, primary, success, warning, error
+ disabled = false,
+ style,
+ ...props
+}) => {
+ const getIconStyle = () => {
+ switch (variant) {
+ case 'primary':
+ return {
+ backgroundColor: colors.primaryLight,
+ iconColor: colors.primary,
+ };
+ case 'success':
+ return {
+ backgroundColor: colors.successLight,
+ iconColor: colors.success,
+ };
+ case 'warning':
+ return {
+ backgroundColor: colors.warningLight,
+ iconColor: colors.warning,
+ };
+ case 'error':
+ return {
+ backgroundColor: colors.errorLight,
+ iconColor: colors.error,
+ };
+ default:
+ return {
+ backgroundColor: colors.surfaceVariant,
+ iconColor: colors.onSurfaceVariant,
+ };
+ }
+ };
+
+ const iconStyle = getIconStyle();
+
+ return (
+
+ );
+};
+
+// Action button with different variants
+export const ActionButton = ({
+ icon,
+ onPress,
+ variant = 'primary',
+ size = 'medium', // small, medium, large
+ disabled = false,
+ style,
+ ...props
+}) => {
+ const getSizeStyle = () => {
+ switch (size) {
+ case 'small':
+ return { width: 40, height: 40, iconSize: 18 };
+ case 'large':
+ return { width: 64, height: 64, iconSize: 32 };
+ default:
+ return { width: 48, height: 48, iconSize: 24 };
+ }
+ };
+
+ const sizeStyle = getSizeStyle();
+
+ return (
+
+ );
+};
+
+// Status indicators
+export const StatusIcon = ({ status, size = 20 }) => {
+ const getStatusConfig = () => {
+ switch (status) {
+ case 'settled':
+ return { icon: 'check-circle', color: colors.success };
+ case 'owed':
+ return { icon: 'arrow-up-circle', color: colors.warning };
+ case 'owes':
+ return { icon: 'arrow-down-circle', color: colors.error };
+ case 'pending':
+ return { icon: 'clock-outline', color: colors.onSurfaceVariant };
+ case 'admin':
+ return { icon: 'crown', color: colors.primary };
+ default:
+ return { icon: 'help-circle', color: colors.onSurfaceVariant };
+ }
+ };
+
+ const config = getStatusConfig();
+
+ return (
+
+ );
+};
+
+// Currency icons
+export const CurrencyIcon = ({ currency = 'INR', size = 16 }) => {
+ const getCurrencyIcon = () => {
+ switch (currency) {
+ case 'USD':
+ return 'currency-usd';
+ case 'EUR':
+ return 'currency-eur';
+ case 'GBP':
+ return 'currency-gbp';
+ case 'INR':
+ default:
+ return 'currency-inr';
+ }
+ };
+
+ return (
+
+ );
+};
+
+// Feature icons with consistent styling
+export const FeatureIcon = ({ feature, size = 24 }) => {
+ const getFeatureIcon = () => {
+ switch (feature) {
+ case 'groups':
+ return 'account-group';
+ case 'expenses':
+ return 'receipt';
+ case 'friends':
+ return 'account-multiple';
+ case 'settings':
+ return 'cog';
+ case 'profile':
+ return 'account-circle';
+ case 'notifications':
+ return 'bell';
+ case 'help':
+ return 'help-circle';
+ case 'feedback':
+ return 'message-star';
+ case 'share':
+ return 'share-variant';
+ case 'export':
+ return 'download';
+ case 'import':
+ return 'upload';
+ case 'sync':
+ return 'sync';
+ case 'security':
+ return 'shield-check';
+ case 'privacy':
+ return 'eye-off';
+ default:
+ return 'circle';
+ }
+ };
+
+ return (
+
+ );
+};
+
+const styles = StyleSheet.create({
+ iconButton: {
+ borderRadius: borderRadius.md,
+ },
+ statusIcon: {
+ margin: 0,
+ },
+ currencyIcon: {
+ margin: 0,
+ },
+});
diff --git a/frontend/utils/inputs.js b/frontend/utils/inputs.js
new file mode 100644
index 00000000..73163375
--- /dev/null
+++ b/frontend/utils/inputs.js
@@ -0,0 +1,234 @@
+import { useState } from 'react';
+import { StyleSheet, TouchableOpacity, View } from 'react-native';
+import { HelperText, IconButton, Text, TextInput } from 'react-native-paper';
+import { hapticFeedback } from './haptics';
+import { colors, spacing, typography } from './theme';
+
+// Enhanced TextInput with floating labels and validation
+export const EnhancedTextInput = ({
+ label,
+ value,
+ onChangeText,
+ error,
+ helperText,
+ leftIcon,
+ rightIcon,
+ onRightIconPress,
+ secureTextEntry = false,
+ style,
+ ...props
+}) => {
+ const [isFocused, setIsFocused] = useState(false);
+ const [isSecure, setIsSecure] = useState(secureTextEntry);
+
+ const handleFocus = () => {
+ setIsFocused(true);
+ hapticFeedback.light();
+ };
+
+ const handleBlur = () => {
+ setIsFocused(false);
+ };
+
+ const toggleSecureEntry = () => {
+ setIsSecure(!isSecure);
+ hapticFeedback.light();
+ };
+
+ return (
+
+
+ {leftIcon && (
+
+ {leftIcon}
+
+ )}
+
+
+
+ {secureTextEntry && (
+
+
+
+ )}
+
+ {rightIcon && !secureTextEntry && (
+
+ {rightIcon}
+
+ )}
+
+
+ {(error || helperText) && (
+
+ {error || helperText}
+
+ )}
+
+ );
+};
+
+// Currency input with automatic formatting
+export const CurrencyInput = ({
+ label = "Amount",
+ value,
+ onChangeText,
+ currency = "₹",
+ style,
+ ...props
+}) => {
+ const formatValue = (text) => {
+ // Remove non-numeric characters except decimal point
+ const numericValue = text.replace(/[^0-9.]/g, '');
+
+ // Ensure only one decimal point
+ const parts = numericValue.split('.');
+ if (parts.length > 2) {
+ return parts[0] + '.' + parts.slice(1).join('');
+ }
+
+ // Limit decimal places to 2
+ if (parts[1] && parts[1].length > 2) {
+ return parts[0] + '.' + parts[1].substring(0, 2);
+ }
+
+ return numericValue;
+ };
+
+ const handleChangeText = (text) => {
+ const formattedValue = formatValue(text);
+ onChangeText(formattedValue);
+ };
+
+ return (
+ {currency}
+ }
+ style={style}
+ {...props}
+ />
+ );
+};
+
+// Search input with clear button
+export const SearchInput = ({
+ placeholder = "Search...",
+ value,
+ onChangeText,
+ onClear,
+ style,
+ ...props
+}) => {
+ const handleClear = () => {
+ onChangeText('');
+ if (onClear) onClear();
+ hapticFeedback.light();
+ };
+
+ return (
+
+ }
+ rightIcon={
+ value ? (
+
+ ) : null
+ }
+ onRightIconPress={value ? handleClear : undefined}
+ style={style}
+ {...props}
+ />
+ );
+};
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginBottom: spacing.md,
+ },
+ inputWrapper: {
+ position: 'relative',
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ textInput: {
+ flex: 1,
+ backgroundColor: colors.surface,
+ },
+ textInputWithLeftIcon: {
+ paddingLeft: 48,
+ },
+ textInputWithRightIcon: {
+ paddingRight: 48,
+ },
+ leftIconContainer: {
+ position: 'absolute',
+ left: 12,
+ zIndex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ rightIconContainer: {
+ position: 'absolute',
+ right: 4,
+ zIndex: 1,
+ },
+ currencySymbol: {
+ ...typography.body1,
+ color: colors.onSurfaceVariant,
+ fontWeight: '600',
+ },
+});
diff --git a/frontend/utils/skeletons.js b/frontend/utils/skeletons.js
new file mode 100644
index 00000000..d7ebb5a1
--- /dev/null
+++ b/frontend/utils/skeletons.js
@@ -0,0 +1,176 @@
+import { useEffect, useRef } from 'react';
+import { Animated, StyleSheet, View } from 'react-native';
+import { borderRadius, colors, spacing } from './theme';
+
+// Shimmer effect for loading skeletons
+const ShimmerEffect = ({ style, children }) => {
+ const shimmerValue = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ const shimmerAnimation = Animated.loop(
+ Animated.sequence([
+ Animated.timing(shimmerValue, {
+ toValue: 1,
+ duration: 1000,
+ useNativeDriver: true,
+ }),
+ Animated.timing(shimmerValue, {
+ toValue: 0,
+ duration: 1000,
+ useNativeDriver: true,
+ }),
+ ])
+ );
+
+ shimmerAnimation.start();
+
+ return () => shimmerAnimation.stop();
+ }, [shimmerValue]);
+
+ const shimmerStyle = {
+ opacity: shimmerValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0.3, 0.7],
+ }),
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Group card skeleton
+export const GroupCardSkeleton = () => (
+
+
+
+
+
+
+
+
+
+
+);
+
+// Expense item skeleton
+export const ExpenseItemSkeleton = () => (
+
+
+
+
+
+
+
+
+);
+
+// Member item skeleton
+export const MemberItemSkeleton = () => (
+
+
+
+
+
+
+
+);
+
+// List of skeletons
+export const SkeletonList = ({ count = 3, SkeletonComponent }) => (
+
+ {Array.from({ length: count }, (_, index) => (
+
+ ))}
+
+);
+
+const styles = StyleSheet.create({
+ skeletonCard: {
+ backgroundColor: colors.surface,
+ borderRadius: borderRadius.lg,
+ padding: spacing.lg,
+ marginBottom: spacing.md,
+ borderWidth: 1,
+ borderColor: colors.outlineVariant,
+ },
+ skeletonHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: spacing.md,
+ },
+ skeletonAvatar: {
+ width: 56,
+ height: 56,
+ borderRadius: 28,
+ backgroundColor: colors.surfaceVariant,
+ marginRight: spacing.md,
+ },
+ skeletonTextContainer: {
+ flex: 1,
+ },
+ skeletonTitle: {
+ height: 20,
+ backgroundColor: colors.surfaceVariant,
+ borderRadius: borderRadius.sm,
+ marginBottom: spacing.xs,
+ width: '70%',
+ },
+ skeletonSubtitle: {
+ height: 14,
+ backgroundColor: colors.surfaceVariant,
+ borderRadius: borderRadius.sm,
+ width: '50%',
+ },
+ skeletonStatus: {
+ height: 32,
+ backgroundColor: colors.surfaceVariant,
+ borderRadius: borderRadius.md,
+ },
+ skeletonExpenseHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: spacing.sm,
+ },
+ skeletonAmount: {
+ height: 20,
+ width: 80,
+ backgroundColor: colors.surfaceVariant,
+ borderRadius: borderRadius.sm,
+ },
+ skeletonChip: {
+ height: 24,
+ width: 120,
+ backgroundColor: colors.surfaceVariant,
+ borderRadius: borderRadius.round,
+ marginBottom: spacing.md,
+ },
+ skeletonMember: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: spacing.sm,
+ },
+ skeletonMemberAvatar: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ backgroundColor: colors.surfaceVariant,
+ marginRight: spacing.md,
+ },
+ skeletonMemberName: {
+ height: 18,
+ backgroundColor: colors.surfaceVariant,
+ borderRadius: borderRadius.sm,
+ marginBottom: spacing.xs,
+ width: '60%',
+ },
+ skeletonMemberRole: {
+ height: 14,
+ backgroundColor: colors.surfaceVariant,
+ borderRadius: borderRadius.sm,
+ width: '40%',
+ },
+});
diff --git a/frontend/utils/theme.js b/frontend/utils/theme.js
new file mode 100644
index 00000000..6bfcf5a6
--- /dev/null
+++ b/frontend/utils/theme.js
@@ -0,0 +1,286 @@
+// Design System for Splitwiser - Following the Blueprint for Gen Z-Centric Fintech Experience
+// Based on "Expressive Minimalism" philosophy with Strategic Glassmorphism
+
+export const colors = {
+ // Primary Palette (Fintech Trust) - Deep, stable colors for security and professionalism
+ background: {
+ primary: '#111827', // Deep blue-gray for dark mode
+ primaryLight: '#FFFFFF', // Pure white for light mode
+ secondary: '#1F2937', // Secondary background for cards, modals (dark)
+ secondaryLight: '#F3F4F6', // Secondary background (light)
+ },
+
+ // Text colors with high contrast for accessibility
+ text: {
+ primary: '#F9FAFB', // Primary text for dark mode
+ primaryLight: '#111827', // Primary text for light mode
+ secondary: '#9CA3AF', // Secondary text for dark mode
+ secondaryLight: '#6B7280', // Secondary text for light mode
+ },
+
+ // Accent Palette (Gen Z Expression) - Bold, high-energy colors
+ brand: {
+ accent: '#8B5CF6', // Vibrant purple - primary accent
+ accentAlt: '#06B6D4', // Electric blue/aqua alternative
+ accentMagenta: '#D946EF', // Viva magenta for bold expressions
+ },
+
+ // Semantic Palette - Standardized system status colors
+ semantic: {
+ success: '#10B981', // Clear, accessible green
+ warning: '#F59E0B', // Amber for warnings
+ error: '#EF4444', // Distinct red for errors/destructive actions
+ },
+
+ // Border and divider colors
+ border: {
+ subtle: '#374151', // Subtle borders for dark mode
+ subtleLight: '#E5E7EB', // Subtle borders for light mode
+ },
+
+ // Glassmorphism-specific colors
+ glass: {
+ background: 'rgba(255, 255, 255, 0.1)', // Semi-transparent background
+ backgroundLight: 'rgba(0, 0, 0, 0.05)', // For light mode
+ border: 'rgba(255, 255, 255, 0.2)', // Subtle border for glass effect
+ borderLight: 'rgba(0, 0, 0, 0.1)', // For light mode
+ },
+};
+
+// CSS Custom Properties (Design Tokens) as specified in the blueprint
+export const tokens = {
+ // Color tokens
+ '--color-background-primary': colors.background.primary,
+ '--color-background-secondary': colors.background.secondary,
+ '--color-text-primary': colors.text.primary,
+ '--color-text-secondary': colors.text.secondary,
+ '--color-brand-accent': colors.brand.accent,
+ '--color-semantic-success': colors.semantic.success,
+ '--color-semantic-error': colors.semantic.error,
+ '--color-border-subtle': colors.border.subtle,
+
+ // Typography tokens
+ '--font-family-primary': "'Inter', sans-serif",
+ '--font-size-display': '48px',
+ '--font-size-h1': '32px',
+ '--font-size-h2': '24px',
+ '--font-size-body': '16px',
+ '--font-size-caption': '12px',
+ '--font-weight-regular': '400',
+ '--font-weight-medium': '500',
+ '--font-weight-semibold': '600',
+ '--font-weight-bold': '700',
+
+ // Spacing tokens (8px base unit system)
+ '--spacing-xs': '4px',
+ '--spacing-sm': '8px',
+ '--spacing-md': '16px',
+ '--spacing-lg': '24px',
+ '--spacing-xl': '32px',
+
+ // Sizing tokens
+ '--border-radius-sm': '4px',
+ '--border-radius-md': '8px',
+ '--border-radius-lg': '16px',
+ '--touch-target-min': '44px',
+};
+
+export const spacing = {
+ xs: 4, // --spacing-xs
+ sm: 8, // --spacing-sm
+ md: 16, // --spacing-md
+ lg: 24, // --spacing-lg
+ xl: 32, // --spacing-xl
+ xxl: 48, // Extended spacing for larger gaps
+};
+
+export const borderRadius = {
+ sm: 4, // --border-radius-sm: For small elements like tags
+ md: 8, // --border-radius-md: For buttons and input fields
+ lg: 16, // --border-radius-lg: For cards and modals
+ round: 999, // For circular elements
+};
+
+// Typography system based on Inter font with clear hierarchy
+export const typography = {
+ // Display text for large, impactful numbers (dashboard balance)
+ display: {
+ fontSize: 48,
+ fontWeight: '700',
+ lineHeight: 56,
+ fontFamily: 'Inter',
+ },
+
+ // Headings
+ h1: {
+ fontSize: 32, // Screen titles
+ fontWeight: '700',
+ lineHeight: 40,
+ fontFamily: 'Inter',
+ },
+ h2: {
+ fontSize: 24, // Section headings
+ fontWeight: '600',
+ lineHeight: 32,
+ fontFamily: 'Inter',
+ },
+ h3: {
+ fontSize: 20,
+ fontWeight: '600',
+ lineHeight: 28,
+ fontFamily: 'Inter',
+ },
+ h4: {
+ fontSize: 18,
+ fontWeight: '600',
+ lineHeight: 24,
+ fontFamily: 'Inter',
+ },
+
+ // Body text
+ body: {
+ fontSize: 16, // Main body text, labels
+ fontWeight: '400',
+ lineHeight: 24,
+ fontFamily: 'Inter',
+ },
+ bodyMedium: {
+ fontSize: 16,
+ fontWeight: '500',
+ lineHeight: 24,
+ fontFamily: 'Inter',
+ },
+
+ // Small text
+ caption: {
+ fontSize: 12, // Small helper text, metadata
+ fontWeight: '400',
+ lineHeight: 16,
+ fontFamily: 'Inter',
+ },
+
+ // Labels and UI text
+ label: {
+ fontSize: 14,
+ fontWeight: '500',
+ lineHeight: 20,
+ fontFamily: 'Inter',
+ },
+};
+
+// Shadows for depth and elevation
+export const shadows = {
+ subtle: {
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 1,
+ },
+ shadowOpacity: 0.05,
+ shadowRadius: 2,
+ elevation: 1,
+ },
+ small: {
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 2,
+ },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ medium: {
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 4,
+ },
+ shadowOpacity: 0.15,
+ shadowRadius: 8,
+ elevation: 4,
+ },
+ large: {
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 8,
+ },
+ shadowOpacity: 0.2,
+ shadowRadius: 16,
+ elevation: 8,
+ },
+};
+
+// Animation timings and easing for consistent motion
+export const animations = {
+ timing: {
+ fast: 150, // Button interactions, quick feedback
+ normal: 250, // Screen transitions, modal appearances
+ slow: 300, // Complex transitions
+ loading: 1500, // Success celebrations
+ },
+
+ easing: {
+ easeOut: 'ease-out',
+ easeIn: 'ease-in',
+ easeInOut: 'ease-in-out',
+ spring: 'cubic-bezier(0.4, 0, 0.2, 1)', // Material Design standard
+ bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
+ },
+};
+
+// Glassmorphism effect properties for strategic application
+export const glassmorphism = {
+ // Background blur and transparency settings
+ blur: {
+ light: 10, // Subtle blur for secondary elements
+ medium: 20, // Standard blur for cards
+ heavy: 40, // Strong blur for overlays
+ },
+
+ // Opacity levels for different contexts
+ opacity: {
+ subtle: 0.1, // Very light transparency
+ light: 0.2, // Light transparency for backgrounds
+ medium: 0.3, // Standard transparency for cards
+ heavy: 0.4, // Stronger transparency for emphasis
+ },
+};
+
+// Paper theme configuration for react-native-paper with blueprint specifications
+export const paperTheme = {
+ colors: {
+ primary: colors.brand.accent,
+ primaryContainer: `${colors.brand.accent}20`, // 20% opacity
+ secondary: colors.brand.accentAlt,
+ secondaryContainer: `${colors.brand.accentAlt}20`,
+ surface: colors.background.secondary,
+ surfaceVariant: colors.background.secondary,
+ background: colors.background.primary,
+ error: colors.semantic.error,
+ onPrimary: '#FFFFFF',
+ onSecondary: '#FFFFFF',
+ onSurface: colors.text.primary,
+ onSurfaceVariant: colors.text.secondary,
+ onError: '#FFFFFF',
+ outline: colors.border.subtle,
+ outlineVariant: `${colors.border.subtle}80`, // 50% opacity
+ },
+ roundness: borderRadius.md,
+};
+
+// Default export for convenient theme access
+export const theme = {
+ colors,
+ spacing,
+ borderRadius,
+ typography,
+ shadows,
+ animations,
+ glassmorphism,
+ paperTheme,
+ tokens,
+};
+
+export default theme;
diff --git a/frontend/utils/toast.js b/frontend/utils/toast.js
new file mode 100644
index 00000000..e35a2238
--- /dev/null
+++ b/frontend/utils/toast.js
@@ -0,0 +1,183 @@
+import { createContext, useContext, useState } from 'react';
+import { Dimensions, StyleSheet, View } from 'react-native';
+import { Snackbar, Text } from 'react-native-paper';
+import { hapticFeedback } from './haptics';
+import { borderRadius, colors, spacing, typography } from './theme';
+
+const { width } = Dimensions.get('window');
+
+// Toast context for global state management
+const ToastContext = createContext();
+
+// Toast types
+export const TOAST_TYPES = {
+ SUCCESS: 'success',
+ ERROR: 'error',
+ WARNING: 'warning',
+ INFO: 'info',
+};
+
+// Toast provider component
+export const ToastProvider = ({ children }) => {
+ const [toast, setToast] = useState(null);
+
+ const showToast = (message, type = TOAST_TYPES.INFO, duration = 4000) => {
+ // Trigger appropriate haptic feedback
+ switch (type) {
+ case TOAST_TYPES.SUCCESS:
+ hapticFeedback.success();
+ break;
+ case TOAST_TYPES.ERROR:
+ hapticFeedback.error();
+ break;
+ case TOAST_TYPES.WARNING:
+ hapticFeedback.warning();
+ break;
+ default:
+ hapticFeedback.light();
+ }
+
+ setToast({
+ message,
+ type,
+ duration,
+ visible: true,
+ });
+ };
+
+ const hideToast = () => {
+ setToast(prev => prev ? { ...prev, visible: false } : null);
+ };
+
+ const getToastStyle = (type) => {
+ switch (type) {
+ case TOAST_TYPES.SUCCESS:
+ return {
+ backgroundColor: colors.success,
+ textColor: 'white',
+ icon: '✅',
+ };
+ case TOAST_TYPES.ERROR:
+ return {
+ backgroundColor: colors.error,
+ textColor: 'white',
+ icon: '❌',
+ };
+ case TOAST_TYPES.WARNING:
+ return {
+ backgroundColor: colors.warning,
+ textColor: 'white',
+ icon: '⚠️',
+ };
+ default:
+ return {
+ backgroundColor: colors.primary,
+ textColor: 'white',
+ icon: 'ℹ️',
+ };
+ }
+ };
+
+ const toastStyle = toast ? getToastStyle(toast.type) : {};
+
+ return (
+
+ {children}
+
+ {toast && (
+
+
+ {toastStyle.icon}
+
+ {toast.message}
+
+
+
+ )}
+
+ );
+};
+
+// Hook to use toast
+export const useToast = () => {
+ const context = useContext(ToastContext);
+ if (!context) {
+ throw new Error('useToast must be used within a ToastProvider');
+ }
+
+ const { showToast, hideToast } = context;
+
+ return {
+ showToast,
+ hideToast,
+ showSuccess: (message, duration) => showToast(message, TOAST_TYPES.SUCCESS, duration),
+ showError: (message, duration) => showToast(message, TOAST_TYPES.ERROR, duration),
+ showWarning: (message, duration) => showToast(message, TOAST_TYPES.WARNING, duration),
+ showInfo: (message, duration) => showToast(message, TOAST_TYPES.INFO, duration),
+ };
+};
+
+// Pre-built toast messages for common scenarios
+export const TOAST_MESSAGES = {
+ SUCCESS: {
+ GROUP_CREATED: 'Group created successfully! 🎉',
+ GROUP_JOINED: 'Successfully joined the group! 🎉',
+ EXPENSE_ADDED: 'Expense added successfully! 💰',
+ EXPENSE_UPDATED: 'Expense updated successfully! ✅',
+ EXPENSE_DELETED: 'Expense deleted successfully! 🗑️',
+ PROFILE_UPDATED: 'Profile updated successfully! ✨',
+ SETTLEMENT_COMPLETED: 'Settlement completed! 🎊',
+ },
+ ERROR: {
+ NETWORK_ERROR: 'Network error. Please check your connection! 📡',
+ INVALID_CODE: 'Invalid group code. Please try again! ❌',
+ INSUFFICIENT_BALANCE: 'Insufficient balance for this transaction! 💳',
+ PERMISSION_DENIED: 'Permission denied! 🚫',
+ GENERIC_ERROR: 'Something went wrong. Please try again! ⚠️',
+ },
+ WARNING: {
+ UNSAVED_CHANGES: 'You have unsaved changes! ⚠️',
+ LOW_BALANCE: 'Your balance is running low! 💰',
+ OFFLINE_MODE: 'You are currently offline! 📱',
+ },
+ INFO: {
+ LOADING: 'Loading... ⏳',
+ SYNC_COMPLETE: 'Data synced successfully! 🔄',
+ FEATURE_COMING_SOON: 'This feature is coming soon! 🚀',
+ },
+};
+
+const styles = StyleSheet.create({
+ snackbar: {
+ marginBottom: spacing.lg,
+ marginHorizontal: spacing.md,
+ borderRadius: borderRadius.lg,
+ maxWidth: width - (spacing.md * 2),
+ },
+ toastContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ toastIcon: {
+ fontSize: 18,
+ marginRight: spacing.sm,
+ },
+ toastMessage: {
+ ...typography.body1,
+ fontWeight: '500',
+ flex: 1,
+ },
+});