Skip to content

SwiftCODA/GameSense-Flutter

Repository files navigation

GameSense Tracker - Flutter Gaming Statistics Tracker (Legacy)

⚠️ This project is now legacy and no longer maintained.

GameSense Tracker was a comprehensive gaming statistics tracker built with Flutter, featuring advanced support for Tom Clancy's Rainbow Six® Siege. The app was previously available on the iOS app store but has been discontinued due to restrictions and limitations imposed by Ubisoft on their API services.

🎉 Success Metrics: Despite its architectural shortcomings, GameSense generated over 300,000 unique downloads before its takedown in 2025, demonstrating strong user engagement and market demand.

📅 Historical Context: This code was initially written in 2022 before I had any university or work experience and was done completely self-taught, hence many of the architectural issues and bad practices documented below. It represents my early learning journey in Flutter development.

This repository is now public as the project is legacy and serves as a code portfolio/reference for Flutter development patterns and techniques - both what to do and what not to do.

Project Shortcomings & Technical Debt

As a legacy project, GameSense has several architectural and development shortcomings that would benefit from refactoring in a modern implementation:

Architecture Issues

  • Lack of separation of concerns: Business logic is tightly coupled with UI components
  • No proper state management: Heavy reliance on setState() instead of proper state management solutions (Bloc, Provider, Riverpod)
  • Mixed responsibilities: UI widgets handle data fetching, business logic, and presentation
  • No dependency injection: Hard dependencies make testing and modularity difficult

Code Quality Issues

  • Large widget files: Many widgets are hundreds of lines long and handle multiple responsibilities
  • Inconsistent error handling: Mix of different error handling patterns throughout the codebase
  • Debug code in production: Excessive debug print statements and logging
  • Hardcoded values: Magic numbers and strings scattered throughout the code
  • Inconsistent naming: Mix of camelCase and other naming conventions

Data Layer Problems

  • No repository pattern: Direct API calls from UI components
  • Poor data caching: Limited offline support and data persistence strategy
  • No data validation: Insufficient input validation and sanitization
  • Mixed data sources: Firebase, REST APIs, and local storage handled inconsistently

Testing Shortcomings

  • Minimal test coverage: Very few unit tests, integration tests, or widget tests
  • Untestable code: Tight coupling makes unit testing difficult
  • No mocking: Direct dependencies on external services without abstraction

Development Practices

  • Large commits: Poor version control practices with large, mixed-purpose commits
  • Inconsistent code style: No enforced code formatting or linting rules
  • No documentation: Limited inline documentation and architectural decision records
  • Security concerns: Potential exposure of sensitive data in debug logs

Performance Issues

  • Excessive rebuilds: Poor widget optimization leading to unnecessary rebuilds
  • Memory leaks: Potential memory leaks from unclosed streams and listeners
  • Large bundle size: Unused dependencies and assets increasing app size
  • Poor list performance: Inefficient list rendering for large datasets

Recommended Improvements for Future Projects

  • Implement Clean Architecture with proper layer separation
  • Use a robust state management solution (Bloc, Riverpod)
  • Add comprehensive testing at all levels
  • Implement proper dependency injection
  • Use repository pattern for data access
  • Add proper error handling and logging
  • Implement offline-first architecture
  • Use code generation for models and routing
  • Add proper CI/CD pipeline with automated testing
  • Implement proper security practices

This project serves as a learning example of how not to structure a large Flutter application, while still demonstrating functional implementation of complex features like in-app purchases, camera integration, and real-time data processing.

It's important to note that this represents early self-taught development work from 2022 and should be viewed as a snapshot of learning progression rather than current best practices.

Features

  • 📊 Real-time Statistics: Track detailed player statistics and performance metrics
  • 📱 Camera Integration: Use device camera to capture and analyze game scoreboards
  • 🎯 Operator Recommendations: AI-powered operator ban suggestions based on opponent data
  • 👥 Social Features: Save friends to favorites and track their progress
  • 📈 Advanced Analytics: View K/D ratios, ranks, win percentages, and seasonal stats
  • 🏆 Leaderboards: Global and friend-based leaderboards
  • 📱 Cross-Platform: Available for both iOS and Android
  • 💰 In-App Purchases: Premium features and customization options
  • 🎮 Multi-Game Support: Extensible architecture for additional games

⚠️ Setup Required

This repository contains placeholder values for security reasons.

You must configure your own credentials before the app will function. See the setup instructions below.

Technology Stack

  • Framework: Flutter
  • Backend: Firebase (Database, Authentication, Crashlytics)
  • APIs: Custom game statistics APIs
  • Payments: In-App Purchases
  • Advertising: Google AdMob
  • Authentication: Firebase Auth with anonymous sign-in
  • Storage: Shared Preferences + Secure Storage
  • Image Processing: Camera integration for scoreboard analysis

Architecture

The app follows a modular architecture with clear separation of concerns:

  • lib/main.dart - App initialization and configuration
  • lib/configs/ - Configuration management and constants
  • lib/utilities/ - Shared utilities and services
  • lib/rainbow_six_siege/ - Game-specific implementations
  • lib/alert_views/ - Reusable UI components
  • lib/main_menu/ - Main navigation and UI
  • lib/settings/ - User preferences and configuration

Getting Started

Prerequisites

  • Flutter SDK (latest stable version)
  • Android Studio / Xcode for platform-specific development
  • Firebase project with required services enabled
  • Valid signing certificates for app store deployment

Installation

  1. Clone the repository:

    git clone https://github.com/your-username/GameSense-Flutter.git
    cd GameSense-Flutter
  2. Install dependencies:

    flutter pub get
  3. Configure environment variables:

    • Copy .env.example to .env
    • Fill in your actual API keys and configuration values (see Configuration below)
  4. Set up Firebase:

    • Replace placeholder values in lib/firebase_options.dart with your Firebase project configuration
    • Add google-services.json (Android) and GoogleService-Info.plist (iOS) from your Firebase console
  5. Run the app:

    flutter run

Project Structure

lib/
├── main.dart                      # App entry point
├── firebase_options.dart          # Firebase configuration
├── configs/                       # App configuration
│   ├── config.dart               # Main config class
│   ├── enums.dart                # App-wide enumerations
│   └── operator_categories.dart   # Game-specific categories
├── utilities/                     # Shared utilities
│   ├── api/                      # API communication
│   ├── firebase/                 # Firebase services
│   ├── in_app_purchases/         # Payment handling
│   ├── storage/                  # Data persistence
│   └── ui/                       # UI components
├── rainbow_six_siege/            # Game-specific features
│   ├── stats/                    # Statistics views
│   └── *.dart                    # Game screens
├── main_menu/                    # Main navigation
├── settings/                     # App settings
└── alert_views/                  # Modal dialogs

Key Features Implementation

In-App Purchases

  • Secure receipt validation with JWT encryption
  • Premium subscription management
  • Consumable purchases (verification badges)
  • Purchase restoration functionality

Advertisement Integration

  • Google AdMob integration
  • Smart ad timing and frequency control
  • Premium user ad removal
  • Banner and interstitial ad support

Camera Integration

  • Real-time scoreboard image capture
  • Image processing for stat extraction
  • Privacy-conscious image handling

Data Security

  • JWT-based API authentication
  • Encrypted local storage for sensitive data
  • Secure HTTP communication
  • Firebase security rules compliance

Configuration

Required Environment Variables

Create a .env file in the project root using .env.example as a template. Key variables include:

# JWT Secret Key (generate a secure random string)
JWT_SECRET_KEY=your-jwt-secret-key-here

# API Endpoints
DOMAIN_API_EPOCH=https://your-api-domain.com/epoch
DOMAIN_API_RECEIPT_VERIFIER=https://your-api-domain.com/verify-receipt
DOMAIN_TERMS_CONDITIONS=https://your-domain.com/terms
DOMAIN_PRIVACY_POLICY=https://your-domain.com/privacy

# In-App Purchase Configuration
IAP_PREMIUM=your.premium.product.id
IAP_CHECKMARK_BADGE=your.badge.product.id
KEY_IAP_HTTP=your-iap-http-encryption-key
KEY_IAP_STORAGE=your-iap-storage-encryption-key
KEY_UUID_STORAGE=your-uuid-storage-encryption-key

# Google AdMob (replace with your actual Ad Unit IDs)
ADS_TESTING=false
ADS_IOS_BANNER=your-ios-banner-ad-unit-id
ADS_ANDROID_BANNER=your-android-banner-ad-unit-id
ADS_IOS_INTERSTITIAL=your-ios-interstitial-ad-unit-id
ADS_ANDROID_INTERSTITIAL=your-android-interstitial-ad-unit-id

# Ad timing and frequency
ADS_MINIMUM_INTERVAL=300
ADS_ADDITIONAL_TIME_MAXIMUM=120
ADS_PROBABILITY_NUMERATOR=1
ADS_PROBABILITY_DENOMINATOR=3

Firebase Configuration

The lib/firebase_options.dart file contains placeholder values. Replace them with your actual Firebase project configuration from the Firebase Console.

⚠️ Important: Never commit your actual Firebase configuration or .env file to version control.

Developer Documentation

Integrating Ads

Guidelines

  • Banner ads must display beneath loading screens.
  • Block ads can appear on loading screens.
  • A single ad placement should only be manually loaded once.
  • Avoid calling setState() unless it's absolutely crucial for updating the UI.

Banner Ads

In order to be able to use the same adUnitId across multiple screens, and avoid unnecessary ad loading, we need to cache the instances of each screen's banner ad(s). The cache is stored in Ads._cachedAds as:

static final Map<String, BannerAd> _cachedAds = {};

where each key follows the format of $adUnitId_$screenKey, and each value is a BannerAd instance. The screenKey is unique to each instance of each screen. It is not re-generated on setState().

Banner ads can be integrated as sticky or natural placements.

  • Sticky: Stays fixed at the bottom of the screen and ignores Widgets behind it, placed in a Stack.
  • Natural: Can be placed in a Column and will move with content, can also be placed in a Column where the preceding child or children is/are wrapped in an Expanded widget, meaning the body content will not be covered, but the banner ad will still stay at the bottom.
Banner (Sticky & Natural) & Block Ad Integration
  1. Import the ads.dart file from 'package:GameSense/extensions/ui/ads/ads.dart'
import 'package:GameSense/extensions/ui/ads/ads.dart';
  1. Define key for the private State class that you want to place the ad in.
  2. Declare a placeholder for an AsyncMemoizer for each ad placement. If placing a banner ad and a block ad, for example, two AsyncMemoizers are needed.
  3. Declare a Future<Widget>? placeholder to pair with each AsyncMemoizer. This will be used to store a single instance of the each Future to minimize instantiation.
  4. Inside the initState() method, give key a value of a new GlobalKey; give _bannerMemoizer and _blockMemoizer values of new AsyncMemoizers.
  5. Ensure that you have declared a bool value called hasPremium in your build() method. This wil be used to determine whether ads should be displayed.
  6. (OPTIONAL) If there are alert Widgets that overlay on top of banner ads, the banner ads should temporarily be hidden while the overlay is shown. In your private State class, declare bool overlayWidgetShown = false;.
class _MainState extends State<Main> {
    // AdMob
    late final GlobalKey key;
    late final AsyncMemoizer _bannerMemoizer;
    late final AsyncMemoizer _blockMemoizer;
    Future<Widget>? buildBannerAd;
    Future<Widget>? buildBlockAd;
    // Whether any overlay widget is shown.
    bool overlayWidgetShown = false;

    @override
    void initState() {
        // AdMob
        key = GlobalKey();
        _bannerMemoizer = AsyncMemoizer();
        _blockMemoizer = AsyncMemoizer();

        super.initState();
    }
}
  1. Inside your state class, declare the following functions. Use one or both as needed:
Future<Widget> _buildBannerAd({required double width}) async =>
    await _bannerMemoizer.runOnce(() async {
        return await Ads.banner(width: width, key: key).widget();
    });

Future<Widget> _buildBlockAd({required double width}) async =>
    await _blockMemoizer.runOnce(() async {
        return await Ads.block(key: key).widget();
    });
  1. In your build() method, declare width with the value of the screen's width and height with the value of the screen's height.

  2. Give your Future placeholders values respectively. Use ??= to give them a new value only if they are null.

  3. (OPTIONAL) If there are alert Widgets that overlay on top of banner ads, the banner ads should temporarily be hidden while the overlay is shown. Give overlayWidgetShown a value of each individual isShown variable for each overlay Widget separated by 'or' statements (||).

    • Eg. overlayWidgetShown = _errorWidgetShwon || _purchaseWidgetShown
final double width = MediaQuery.of(context).size.width;
final double height = MediaQuery.of(context).size.height;

// AdMob
buildBannerAd ??= _buildBannerAd(width: width);
buildBlockAd ??= _buildBlockAd(width: width);
// Show/hide banner ads when overlay widgets are shown/hidden.
overlayWidgetShown = CONDITIONAL_STATEMENT_HERE;
Sticky Banner Ad Integration
  1. Create a Stack in your body.
  2. Add your main body Widget(s) as the first child or children.
  3. Add the following so your Stack looks something like this:
Stack(
    children: [
        // Your main body Widget(s).
        ...,
        // If the user doesn't have Premium, and should see ads.
        if (!hasPremium && !overlayWidgetShown)
        SizedBox(
            // Gives the banner ad the ability to stick to the bottom of the screen.
            height: height,
            child: Column(
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
                    // The Spacer will force the banner to stay at the bottom.
                    const Spacer(),
                    FutureBuilder<Widget>(
                        // Your previously defined "buildBannerAd" instance.
                        future: buildBannerAd,
                        builder: (_, snapshot) {
                            return snapshot.hasData
                                // Shows the banner ad.
                                ? snapshot.data!
                                // Shows nothing when the ad is not loaded.
                                : Ads.banner(width: width, key: key)
                                    .defaultWidget();
                        }
                    )
                ]
            )
        )
    ]
)
Natural Banner Ad Integration
  1. Create a Column in your body.
  2. Add your main body Widget(s) as the first child or children. OPTIONAL: Wrap one of those Widgets with an Expanded(child: ...) to force the natural banner ad to stay at the bottom.
  3. Add the following so your Column looks something like this:
Column(
    children: [
        // Your expanded body widget. Other widgets can be above or below this too.
        Expanded(
            child: ...
        ),
        // If the user doesn't have Premium, and should see ads.
        if (!hasPremium && !overlayWidgetShown)
        FutureBuilder<Widget>(
            // Your previously defined "buildBannerAd" instance.
            future: buildBannerAd,
            builder: (_, snapshot) {
                return snapshot.hasData
                    // Shows the banner ad.
                    ? snapshot.data!
                    // Shows nothing when the ad is not loaded.
                    : Ads.banner(width: width, key: key)
                        // Specify styling for the placeholder
                        .defaultWidget(
                            backgroundColor: MyColors.r6grey,
                            textColor: MyColors.r6light
                        );
            }
        )
    ]
)
Block Ad Integration

Block ads should avoid having Widgets on either side of them. Block ads should be placed in Columns.

  1. Create a Column in your body.
  2. Add your main body Widget(s) as the first child or children.
  3. Add the following so your Column looks something like this:
Column(
    children: [
        // Your main body Widget(s).
        ...,
        // If the user doesn't have Premium, and should see ads.
        if (!hasPremium && !overlayWidgetShown)
        FutureBuilder<Widget>(
            // Your previously defined "buildBlockAd" instance.
            future: buildBlockAd,
            builder: (_, snapshot) {
                return snapshot.hasData
                    // Shows the banner ad.
                    ? snapshot.data!
                    // Shows nothing when the ad is not loaded.
                    : const SizedBox();
            }
        )
    ]
)

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Security

This project implements several security measures:

  • Environment-based configuration management
  • Encrypted data storage
  • Secure API communication
  • Production-ready authentication flows

If you discover any security vulnerabilities, please report them responsibly.

License

This project is licensed for viewing purposes only.
All code and assets © 2020–2025 Jarren Morris.
See the LICENSE file for full details.

Acknowledgments

  • Built with Flutter and Firebase
  • Rainbow Six Siege statistics integration
  • Google AdMob for monetization
  • Various open-source Flutter packages

Disclaimer

This project is not affiliated with or endorsed by Ubisoft Entertainment. Tom Clancy's Rainbow Six® Siege is a trademark of Ubisoft Entertainment.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages