IWS Version Manager

IWS Version Manager makes it easy to notify users about new app versions, manage version compatibility, and block versions that are no longer supported. This package provides comprehensive version management with automatic notifications, device tracking, and flexible UI components.

Features

  • Automatic Version Checking: Automatically check for new versions on app startup
  • Multiple Notification Types: Support for mandatory and optional updates
  • Rich UI Components: Pre-built widgets for different notification styles
  • Release Notes: Display formatted release notes with markdown support
  • Device Tracking: Track device usage and user analytics
  • Platform Support: Cross-platform support with web-specific features
  • Custom Release Note Rendering: Customizable release note display
  • Connection Error Handling: Graceful handling of network issues
  • BLoC State Management: Built-in state management for version status

Getting Started

1. Environment Setup

Add the IWS API key and Version Code as DART variables at compile time:

flutter run --dart-define=IWS_API_KEY=your_api_key_here --dart-define=APP_VERSION_CODE=1

Or add them to your launch.json for VS Code:

{
  "configurations": [
    {
      "name": "Flutter",
      "type": "dart",
      "request": "launch",
      "program": "lib/main.dart",
      "args": [
        "--dart-define=IWS_API_KEY=your_api_key_here",
        "--dart-define=APP_VERSION_CODE=1"
      ]
    }
  ]
}

2. Initialize Version Manager

Initialize the version checking in your app startup:

void main() {
  runApp(MyApp());
  
  // Initialize version manager
  if (!IwsVersionManager().widgetsInitialized()) {
    IwsVersionManager(
      deviceId: 'unique-device-id',
      userIdentifier: 'user-identifier'
    ).initializeWidgets();
  }
}

3. Configure Notification & Platform Behavior (Optional)

You can customize on which platforms version notices appear and how notices behave when there is no download URL:

IwsVersionManager(
  // Filter which platforms show version notifications/dialogs (null = all platforms enabled)
  enabledNotificationPlatforms: {
    PlatformName.android,
    PlatformName.ios,
  },
  // Behavior when there is no download URL (never by default)
  noDownloadUrlNoticeMode: NoDownloadUrlNoticeMode.never,
).initializeWidgets();

Available Notice Modes (NoDownloadUrlNoticeMode):

  • NoDownloadUrlNoticeMode.never: (Default) Does not show version banners or dialogs when no download URL is available.
  • NoDownloadUrlNoticeMode.mandatoryOnly: Shows version notices without a download URL only when the update is mandatory (mandatoryUpdate: true).
  • NoDownloadUrlNoticeMode.always: Always shows version notices even when no download URL is available.

Platform Filtering (enabledNotificationPlatforms):

Pass a Set<PlatformName> to specify the allowed platforms (e.g. {PlatformName.android, PlatformName.ios}). If the app runs on a platform not included in the set, version widgets and notifications will not be displayed.

4. Configure Web Server Cache Headers (Required for Web)

For Flutter Web apps to properly update, you should configure your web server to avoid caching the app shell and bootstrap entry points too aggressively. Flutter's official FAQ explains that browsers and CDNs can keep serving stale files after deployment, and recommends cache-busting or filename changes for static resources.

Official reference: Why doesn't my app update immediately after it's deployed?

These files change with every build and should be fetched fresh:

File Cache Header
index.html Cache-Control: no-cache, no-store, must-revalidate
flutter_service_worker.js Cache-Control: no-cache, no-store, must-revalidate
flutter.js or flutter_bootstrap.js Cache-Control: no-cache, no-store, must-revalidate

Firebase Hosting

Add a firebase.json configuration file to your project root:

{
  "hosting": {
    "public": "build/web",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "headers": [
      {
        "source": "index.html",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "no-cache, no-store, must-revalidate"
          }
        ]
      },
      {
        "source": "flutter_service_worker.js",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "no-cache, no-store, must-revalidate"
          }
        ]
      },
      {
        "source": "flutter.js",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "no-cache, no-store, must-revalidate"
          }
        ]
      }
    ]
  }
}

Shared Hosting (Apache - .htaccess)

Create or modify the .htaccess file in your build/web directory:

<Files "index.html">
    Header set Cache-Control "no-cache, no-store, must-revalidate"
    Header set Pragma "no-cache"
    Header set Expires "0"
</Files>

<Files "flutter_service_worker.js">
    Header set Cache-Control "no-cache, no-store, must-revalidate"
    Header set Pragma "no-cache"
    Header set Expires "0"
</Files>

<Files "flutter.js">
    Header set Cache-Control "no-cache, no-store, must-revalidate"
    Header set Pragma "no-cache"
    Header set Expires "0"
</Files>

Shared Hosting (Nginx - nginx.conf)

Configure your server block in nginx.conf:

server {
    listen 80;
    server_name your-domain.com;
    root /var/www/build/web;

    location = /index.html {
        add_header Cache-Control "no-cache, no-store, must-revalidate" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
    }

    location = /flutter_service_worker.js {
        add_header Cache-Control "no-cache, no-store, must-revalidate" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
    }

    location = /flutter.js {
        add_header Cache-Control "no-cache, no-store, must-revalidate" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
    }
}

Other Hosting Providers

For other hosting providers, check their documentation for setting custom HTTP headers. Common ways include:

  • Control panel/Dashboard headers configuration
  • Custom headers in deployment configuration files
  • .htaccess or web.config files
  • Hosting provider's API for header management

⚠️ Warning: Implications of No-Cache Headers

Setting no-cache, no-store, must-revalidate headers on these files has important implications:

  1. Bandwidth Impact: These files will be downloaded on every page load, even for users who recently visited. This increases bandwidth usage compared to cached versions.

  2. Slower Load Times: Users may experience slightly slower page loads if they don't have a local browser cache, as the files must be fetched from the server every time.

  3. Server Load: Increased server requests may result in higher server load, especially with high traffic volumes.

  4. Browser Storage: The must-revalidate directive ensures that even expired cached versions are revalidated with the server, preventing stale versions from being served.

  5. User Bandwidth: Users with metered connections (mobile data) may experience higher data usage.

These tradeoffs are necessary for proper update functionality. Without these headers, users may not receive critical updates and could experience inconsistent behavior with different versions of your app running simultaneously. The benefits of reliable updates outweigh the performance considerations in most cases.

Optimization Tip: You can cache other assets normally when they are versioned or fingerprinted by your build process. The main goal is to avoid stale app-shell files and to use cache-busting for resources that must change immediately after deployment.

Usage Examples

1. Automatic Scaffold Messages (Modern Responsive Notifications)

The easiest way to show version notifications is using IwsVersionScaffoldMessage. It provides a modern, responsive, zero-dependency notification overlay that automatically adapts to Mobile (Android, iOS) and Desktop/Web (Windows, macOS, Linux, Web):

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return IwsVersionScaffoldMessage(
      showConnectionErrors: true, // Show connection error notifications
      notificationStyle: IwsNotificationStyle.adaptive, // adaptive, floatingToast, snackBar, dialog, banner
      position: IwsNotificationPosition.adaptive, // adaptive (topRight on Desktop/Web, top on Mobile)
      autoDismissDuration: const Duration(seconds: 8),
      child: Scaffold(
        appBar: AppBar(title: Text('My App')),
        body: YourContent(),
      ),
    );
  }
}

Notification Styles (IwsNotificationStyle):

  • IwsNotificationStyle.adaptive: (Default & Recommended) Renders a floating responsive card in the top-right corner on Web/Desktop, a top floating card on Mobile, and a blocking modal dialog for mandatory updates.
  • IwsNotificationStyle.floatingToast: Floating animated card with auto-dismiss and swipe-to-dismiss support.
  • IwsNotificationStyle.snackBar: Uses standard Material 3 ScaffoldMessenger.of(context).showSnackBar.
  • IwsNotificationStyle.dialog: Shows an AlertDialog for all version updates.
  • IwsNotificationStyle.banner: In-app top banner bar.

Theme Customization (IwsNotificationTheme):

You can customize colors, borders, typography, and button labels:

IwsVersionScaffoldMessage(
  theme: const IwsNotificationTheme(
    downloadButtonText: 'ACTUALIZAR AHORA',
    releaseNotesButtonText: 'Ver cambios',
    elevation: 6.0,
    borderRadius: BorderRadius.all(Radius.circular(16.0)),
  ),
  child: Scaffold(...),
);

2. Version Icon Button

Add a version status icon to your app bar:

class MyAppBar extends StatelessWidget implements PreferredSizeWidget {
  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: Text('My App'),
      actions: [
        IwsVersionIconButton(
          showConnectionError: true, // Show icon for connection errors
        ),
      ],
    );
  }
  
  @override
  Size get preferredSize => Size.fromHeight(kToolbarHeight);
}

3. Custom Version Listener

Handle version events with custom logic:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return IwsVersionListener(
      onUpdateAvailable: (lastVersion, downloadLink) {
        // Handle optional update
        print('Update available: ${lastVersion?.versionName}');
        // Show custom UI or trigger custom logic
      },
      onMandatoryUpdate: (lastVersion, downloadLink) {
        // Handle mandatory update
        showDialog(
          context: context,
          barrierDismissible: false,
          builder: (context) => AlertDialog(
            title: Text('Update Required'),
            content: Text('You must update to continue using the app.'),
            actions: [
              TextButton(
                onPressed: () => _launchUpdate(downloadLink),
                child: Text('Update Now'),
              ),
            ],
          ),
        );
      },
      onConnectionError: () {
        // Handle connection errors
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Connection error')),
        );
      },
      onPlatformNotSupported: () {
        // Handle unsupported platform
        print('Platform not supported');
      },
      child: YourAppContent(),
    );
  }
  
  void _launchUpdate(String? downloadLink) {
    if (downloadLink != null) {
      // Launch download URL
    }
  }
}

4. Custom Version Builder

Build different UI based on version status:

class VersionAwareWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return IwsVersionBuilder(
      updateAvailable: (lastVersion) {
        return Card(
          color: Colors.green.shade100,
          child: Padding(
            padding: EdgeInsets.all(16.0),
            child: Column(
              children: [
                Icon(Icons.new_releases, color: Colors.green),
                Text('Version ${lastVersion?.versionName} available!'),
                ElevatedButton(
                  onPressed: () => _downloadUpdate(),
                  child: Text('Download'),
                ),
              ],
            ),
          ),
        );
      },
      mandatoryUpdate: (lastVersion) {
        return Container(
          color: Colors.orange.shade100,
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.warning, size: 64, color: Colors.orange),
                Text('Mandatory Update Required'),
                Text('Version: ${lastVersion?.versionName}'),
                ElevatedButton(
                  onPressed: () => _downloadUpdate(),
                  child: Text('Update Now'),
                ),
              ],
            ),
          ),
        );
      },
      connectionError: () {
        return Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.wifi_off, size: 64, color: Colors.red),
              Text('Connection Error'),
              ElevatedButton(
                onPressed: () => _retryConnection(),
                child: Text('Retry'),
              ),
            ],
          ),
        );
      },
      platformNotSupported: () {
        return Center(
          child: Text('Platform not supported'),
        );
      },
      child: YourNormalContent(),
    );
  }
  
  void _downloadUpdate() {
    // Handle download logic
  }
  
  void _retryConnection() {
    IwsVersionManager().versionBloc.add(CheckAppVersion());
  }
}

5. Manual Version Checking

Check for versions programmatically:

class VersionService {
  static Future<void> checkForUpdates() async {
    try {
      final versionStatus = await IwsVersionManager(
        deviceId: 'device-id',
        userIdentifier: 'user-id',
      ).checkPlatformVersion();
      
      if (versionStatus.newVersionAvailable) {
        print('New version available: ${versionStatus.lastVersion?.versionName}');
        
        if (versionStatus.mandatoryUpdate) {
          print('This is a mandatory update!');
          // Force user to update
        } else {
          print('Optional update available');
          // Show optional update notification
        }
        
        // Access version details
        final version = versionStatus.lastVersion;
        if (version != null) {
          print('Version Code: ${version.versionCode}');
          print('Version Name: ${version.versionName}');
          print('Download Link: ${version.downloadLink}');
          print('Release Notes: ${version.rawReleaseNotes}');
          print('Published At: ${version.publishedAt}');
          print('Minimal Version: ${version.minimalVersion}');
        }
      }
      
      if (!versionStatus.supportedPlatform) {
        print('Current platform is not supported');
      }
      
    } catch (e) {
      if (e is SdkNotConfigureException) {
        print('SDK not configured: ${e.message}');
      } else {
        print('Error checking version: $e');
      }
    }
  }
}

6. Custom Release Note Rendering

Customize how release notes are displayed:

void setupCustomReleaseNotes() {
  IwsVersionManager().releaseNoteBuilder = (String plainText, dynamic richText) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'What\'s New',
          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
        ),
        SizedBox(height: 8),
        Container(
          padding: EdgeInsets.all(12),
          decoration: BoxDecoration(
            color: Colors.blue.shade50,
            borderRadius: BorderRadius.circular(8),
          ),
          child: MarkdownBody(data: plainText),
        ),
      ],
    );
  };
}

7. BLoC Integration

Access the version BLoC directly for advanced use cases:

class CustomVersionWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<VersionBloc, VersionState>(
      bloc: IwsVersionManager().versionBloc,
      builder: (context, state) {
        switch (state.status) {
          case VersionStatus.starting:
            return CircularProgressIndicator();
          case VersionStatus.updated:
            return Text('App is up to date');
          case VersionStatus.updateAvailable:
            return _buildUpdateAvailable(state);
          case VersionStatus.mandatoryUpdate:
            return _buildMandatoryUpdate(state);
          case VersionStatus.connectionError:
            return _buildConnectionError();
          case VersionStatus.platformNotSupported:
            return _buildPlatformNotSupported();
        }
      },
    );
  }
  
  Widget _buildUpdateAvailable(VersionState state) {
    return Card(
      child: ListTile(
        leading: Icon(Icons.new_releases, color: Colors.green),
        title: Text('Update Available'),
        subtitle: Text('Version ${state.lastVersion?.versionName}'),
        trailing: TextButton(
          onPressed: () => _downloadUpdate(state.downloadLink),
          child: Text('Download'),
        ),
      ),
    );
  }
  
  Widget _buildMandatoryUpdate(VersionState state) {
    return Card(
      color: Colors.orange.shade100,
      child: ListTile(
        leading: Icon(Icons.warning, color: Colors.orange),
        title: Text('Mandatory Update'),
        subtitle: Text('Update required to continue'),
        trailing: ElevatedButton(
          onPressed: () => _downloadUpdate(state.downloadLink),
          child: Text('Update'),
        ),
      ),
    );
  }
  
  Widget _buildConnectionError() {
    return Card(
      child: ListTile(
        leading: Icon(Icons.error, color: Colors.red),
        title: Text('Connection Error'),
        trailing: IconButton(
          onPressed: () => _retryCheck(),
          icon: Icon(Icons.refresh),
        ),
      ),
    );
  }
  
  Widget _buildPlatformNotSupported() {
    return Card(
      child: ListTile(
        leading: Icon(Icons.block, color: Colors.grey),
        title: Text('Platform Not Supported'),
        subtitle: Text('This platform is not supported'),
      ),
    );
  }
  
  void _downloadUpdate(String? downloadLink) {
    // Implementation
  }
  
  void _retryCheck() {
    IwsVersionManager().versionBloc.add(CheckAppVersion());
  }
}

Configuration Options

Version Status Types

The package provides several version status types:

  • VersionStatus.starting: Initial state, checking in progress
  • VersionStatus.updated: App is up to date
  • VersionStatus.updateAvailable: Optional update available
  • VersionStatus.mandatoryUpdate: Required update available
  • VersionStatus.connectionError: Network connection error
  • VersionStatus.platformNotSupported: Platform not supported

Widget Configuration

Each widget supports various configuration options:

IwsVersionScaffoldMessage

  • showConnectionErrors: Show notifications for connection errors
  • messageHeight: Height of notification messages
  • noDownloadUrlNoticeMode: Override notice mode for when there is no download URL
  • enabledPlatforms: Override allowed platforms for displaying scaffold notifications
  • customNoDownloadUrlMessage: Optional custom text to display when there is no download link

IwsVersionIconButton

  • showConnectionError: Show icon for connection errors
  • noDownloadUrlNoticeMode: Override notice mode for when there is no download URL
  • enabledPlatforms: Override allowed platforms for displaying version icon
  • customNoDownloadUrlMessage: Optional custom text to display when there is no download link

Error Handling

The package includes built-in error handling:

try {
  final status = await IwsVersionManager().checkPlatformVersion();
  // Handle status
} on SdkNotConfigureException catch (e) {
  // Handle configuration errors
  print('Configuration error: ${e.message}');
} on FetchDataException catch (e) {
  // Handle network errors
  print('Network error: ${e.httpCode}');
} catch (e) {
  // Handle other errors
  print('Unknown error: $e');
}

Data Models

ApplicationVersion

class ApplicationVersion {
  int versionCode;           // Numeric version code
  String versionName;        // Human-readable version name
  dynamic releaseNotes;      // Rich text release notes
  String? rawReleaseNotes;   // Plain text release notes
  String? downloadLink;      // Direct download URL
  DateTime? publishedAt;     // Publication date
  int minimalVersion;        // Minimum supported version
  String? platformDownloadLink; // Platform-specific download URL
}

AppVersionStatus

class AppVersionStatus {
  bool newVersionAvailable;  // New version is available
  bool mandatoryUpdate;      // Update is mandatory
  bool supportedPlatform;    // Platform is supported
  ApplicationVersion? lastVersion; // Latest version information
}

Web Support

The package provides unified version handling on Flutter Web:

  • Download URL Support: If a download URL (downloadLink / platformDownloadLink) is configured, a download action is presented to the user.
  • Configurable Notices: When no download URL exists, NoDownloadUrlNoticeMode controls whether notices are shown (hidden by default).
  • Custom Messages: Support for custom descriptions or instructions via customNoDownloadUrlMessage.

Best Practices

  1. Initialize Early: Call initializeWidgets() as early as possible in your app lifecycle
  2. Handle Errors: Always implement error handling for network issues
  3. Test Updates: Use the dry-run publish option to test your update flow
  4. User Experience: Provide clear messaging about update requirements
  5. Graceful Degradation: Ensure your app works even when version checking fails

Advanced Usage

Custom BLoC Events

Trigger version checks manually:

// Trigger a version check
IwsVersionManager().versionBloc.add(CheckAppVersion());

State Monitoring

Monitor version state changes:

IwsVersionManager().versionBloc.stream.listen((state) {
  print('Version status changed: ${state.status}');
});