IWS Agreements Package

A comprehensive Flutter package for managing user agreements, terms of service, privacy policies, and other legal documents. This package provides a complete solution for displaying, tracking, and managing user agreement acceptance with version control and device information tracking.

Features

  • Agreement Management: Display and retrieve the latest versions of published agreements
  • User Tracking: Track which agreements users have accepted and when
  • Pending Agreements: Identify agreements that need user acceptance or have newer versions
  • Bulk Operations: Accept multiple agreements at once
  • Device Information: Track device details when agreements are accepted
  • Version Control: Handle agreement versioning and changes over time
  • UI Components: Pre-built widgets for displaying agreement lists
  • Signature Support: Optional signature tracking for agreement acceptance

Getting started

Configuration

Add the IWS API key as a DART variable at compile time:

--dart-define=IWS_API_KEY=your_api_key_here

Data Models

AgreementVersion

Represents a specific version of an agreement with all its metadata:

class AgreementVersion {
  int id;
  int version;
  String? content;           // Delta formatted content (Quill)
  String? rawContent;        // Plain text content
  bool published;
  DateTime? startDate;       // When this version becomes active
  DateTime? endDate;         // When this version expires
  String? summaryOfChanges;  // Description of changes in this version
  Agreement agreement;       // Basic agreement information
  DateTime? createdAt;
  DateTime? updatedAt;
}

Agreement

Basic agreement information:

class Agreement {
  int id;
  String name;               // Unique identifier
  String title;              // Display title
  bool requiresAcceptance;   // Whether user must explicitly accept
  DateTime? createdAt;
  DateTime? updatedAt;
}

PendingAgreement

Represents an agreement that needs user action:

class PendingAgreement {
  AgreementVersion agreementVersion;
  bool isNewVersion;         // true if user has accepted older version
}

AcceptedAgreement

Contains user's agreement acceptance history:

class AcceptedAgreement {
  List<Accepted> accepted;           // List of user's accepted agreements
  List<AgreementVersion> lastAgreements; // Latest versions of all agreements
}

Accepted

Individual agreement acceptance record:

class Accepted {
  int id;
  String agreementName;
  int agreementVersion;
  DateTime acceptatedAt;     // When user accepted
  String? signature;         // Optional signature
}

Usage

Initialize the Service

The IwsAgreements class is a singleton that handles all agreement operations:

import 'package:iws_agreements/iws_agreements.dart';

final iwsAgreements = IwsAgreements();

Retrieve All Available Agreements

Get the latest published versions of all agreements:

try {
  final List<AgreementVersion> agreements = await iwsAgreements.getAllAgreements();
  
  for (var agreement in agreements) {
    print('Agreement: ${agreement.agreement.title}');
    print('Version: ${agreement.version}');
    print('Requires Acceptance: ${agreement.agreement.requiresAcceptance}');
    print('Content: ${agreement.content}');
  }
} catch (e) {
  print('Error retrieving agreements: $e');
}

Check User's Accepted Agreements

See which agreements a specific user has already accepted:

try {
  final AcceptedAgreement userAgreements = await iwsAgreements
      .getAcceptedAgreements('user_123');
  
  print('User has accepted ${userAgreements.accepted.length} agreements');
  
  for (var accepted in userAgreements.accepted) {
    print('Accepted: ${accepted.agreementName} v${accepted.agreementVersion}');
    print('Accepted on: ${accepted.acceptatedAt}');
  }
} catch (e) {
  print('Error retrieving accepted agreements: $e');
}

Get Pending Agreements for User

Identify agreements that need user attention (not accepted or newer versions available):

try {
  // Get all pending agreements
  final List<PendingAgreement> allPending = await iwsAgreements
      .getPendingAgreements('user_123');
  
  // Get only agreements that require acceptance
  final List<PendingAgreement> requiredPending = await iwsAgreements
      .getPendingAgreements('user_123', onlyWithRequiredAcceptance: true);
  
  for (var pending in requiredPending) {
    if (pending.isNewVersion) {
      print('New version available for: ${pending.agreementVersion.agreement.title}');
    } else {
      print('Not yet accepted: ${pending.agreementVersion.agreement.title}');
    }
    print('Summary of changes: ${pending.agreementVersion.summaryOfChanges}');
  }
} catch (e) {
  print('Error retrieving pending agreements: $e');
}

Get Specific Agreement

Retrieve the latest version of a specific agreement by name:

try {
  final AgreementVersion? agreement = await iwsAgreements
      .getAgreement('terms_of_service');
  
  if (agreement != null) {
    print('Latest version: ${agreement.version}');
    print('Content: ${agreement.content}');
    print('Active from: ${agreement.startDate}');
  } else {
    print('Agreement not found');
  }
} catch (e) {
  print('Error retrieving agreement: $e');
}

Accept Single Agreement

Record user acceptance of a specific agreement:

import 'package:iws_device_info/iws_device_info.dart';

try {
  // Basic acceptance
  await iwsAgreements.acceptAgreement(
    'privacy_policy',
    userIdentifier: 'user_123',
  );
  
  // Acceptance with additional information
  final device = await AppDevice.init();
  await iwsAgreements.acceptAgreement(
    'terms_of_service',
    userIdentifier: 'user_123',
    device: device,
    appVersionCode: 123,
    signature: 'user_digital_signature',
  );
  
  print('Agreement accepted successfully');
} catch (e) {
  print('Error accepting agreement: $e');
}

Accept Multiple Agreements

Accept several agreements at once:

try {
  final agreementsToAccept = [
    'terms_of_service',
    'privacy_policy',
    'cookie_policy'
  ];
  
  final device = await AppDevice.init();
  
  await iwsAgreements.acceptAgreements(
    agreementsToAccept,
    userIdentifier: 'user_123',
    device: device,
    appVersionCode: 123,
  );
  
  print('All agreements accepted successfully');
} catch (e) {
  print('Error accepting agreements: $e');
}

Using the Agreement List Widget

Display agreements in your UI using the pre-built widget:

import 'package:flutter/material.dart';
import 'package:iws_agreements/iws_agreements.dart';

class AgreementsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Agreements')),
      body: IwsAgreementList(
        // Optional: Custom item builder
        itemBuilder: (AgreementVersion agreement) {
          return Card(
            margin: EdgeInsets.all(8.0),
            child: ListTile(
              title: Text(agreement.agreement.title),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Version: ${agreement.version}'),
                  if (agreement.summaryOfChanges != null)
                    Text('Changes: ${agreement.summaryOfChanges}'),
                ],
              ),
              trailing: agreement.agreement.requiresAcceptance
                  ? Icon(Icons.warning, color: Colors.orange)
                  : Icon(Icons.info, color: Colors.blue),
              onTap: () {
                // Navigate to agreement details
                _showAgreementDetails(context, agreement);
              },
            ),
          );
        },
      ),
    );
  }
  
  void _showAgreementDetails(BuildContext context, AgreementVersion agreement) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(agreement.agreement.title),
        content: SingleChildScrollView(
          child: Text(agreement.content ?? 'No content available'),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('Close'),
          ),
          if (agreement.agreement.requiresAcceptance)
            ElevatedButton(
              onPressed: () async {
                // Accept the agreement
                await IwsAgreements().acceptAgreement(
                  agreement.agreement.name,
                  userIdentifier: 'current_user_id',
                );
                Navigator.of(context).pop();
              },
              child: Text('Accept'),
            ),
        ],
      ),
    );
  }
}

Complete User Flow Example

Here's a complete example showing how to check for pending agreements and handle user acceptance:

import 'package:flutter/material.dart';
import 'package:iws_agreements/iws_agreements.dart';
import 'package:iws_device_info/iws_device_info.dart';

class AgreementManager {
  final IwsAgreements _iwsAgreements = IwsAgreements();
  
  /// Check if user needs to accept any agreements
  Future<bool> hasUserAcceptedAllRequiredAgreements(String userId) async {
    try {
      final pendingAgreements = await _iwsAgreements.getPendingAgreements(
        userId,
        onlyWithRequiredAcceptance: true,
      );
      
      return pendingAgreements.isEmpty;
    } catch (e) {
      print('Error checking agreements: $e');
      return false;
    }
  }
  
  /// Show pending agreements to user and handle acceptance
  Future<void> showPendingAgreements(BuildContext context, String userId) async {
    try {
      final pendingAgreements = await _iwsAgreements.getPendingAgreements(
        userId,
        onlyWithRequiredAcceptance: true,
      );
      
      if (pendingAgreements.isEmpty) {
        return; // No pending agreements
      }
      
      final agreementsToAccept = <String>[];
      
      for (var pending in pendingAgreements) {
        final shouldAccept = await _showAgreementDialog(
          context,
          pending.agreementVersion,
          pending.isNewVersion,
        );
        
        if (shouldAccept) {
          agreementsToAccept.add(pending.agreementVersion.agreement.name);
        }
      }
      
      if (agreementsToAccept.isNotEmpty) {
        await _acceptAgreements(userId, agreementsToAccept);
      }
    } catch (e) {
      print('Error handling pending agreements: $e');
    }
  }
  
  Future<bool> _showAgreementDialog(
    BuildContext context,
    AgreementVersion agreement,
    bool isNewVersion,
  ) async {
    return await showDialog<bool>(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: Text(agreement.agreement.title),
        content: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              if (isNewVersion)
                Container(
                  padding: EdgeInsets.all(8),
                  decoration: BoxDecoration(
                    color: Colors.orange.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(4),
                  ),
                  child: Row(
                    children: [
                      Icon(Icons.update, color: Colors.orange),
                      SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          'This is a new version of an agreement you previously accepted.',
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                      ),
                    ],
                  ),
                ),
              if (isNewVersion) SizedBox(height: 16),
              if (agreement.summaryOfChanges != null) ...[
                Text(
                  'Summary of Changes:',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                Text(agreement.summaryOfChanges!),
                SizedBox(height: 16),
              ],
              Text(
                'Agreement Content:',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              SizedBox(height: 8),
              Container(
                height: 200,
                child: SingleChildScrollView(
                  child: Text(agreement.content ?? 'No content available'),
                ),
              ),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: Text('Decline'),
          ),
          ElevatedButton(
            onPressed: () => Navigator.of(context).pop(true),
            child: Text('Accept'),
          ),
        ],
      ),
    ) ?? false;
  }
  
  Future<void> _acceptAgreements(String userId, List<String> agreements) async {
    try {
      final device = await AppDevice.init();
      
      await _iwsAgreements.acceptAgreements(
        agreements,
        userIdentifier: userId,
        device: device,
        appVersionCode: 100, // Your app version
      );
      
      print('Successfully accepted ${agreements.length} agreements');
    } catch (e) {
      print('Error accepting agreements: $e');
      rethrow;
    }
  }
}

Error Handling

The package uses the iws_http library for API communication. Common errors include:

  • DataNotFoundException: When a specific agreement is not found
  • Network errors: When API is unreachable
  • Authentication errors: When API key is invalid

Always wrap API calls in try-catch blocks:

try {
  final agreements = await iwsAgreements.getAllAgreements();
  // Handle success
} on DataNotFoundException {
  // Handle specific not found error
} catch (e) {
  // Handle general errors
  print('Error: $e');
}

Best Practices

  1. Check for required agreements on app start
  2. Use device information for compliance tracking
  3. Handle network errors gracefully
  4. Store user acceptance status locally for offline access
  5. Show agreement summaries for version updates
  6. Implement proper loading states in your UI

API Configuration

The package connects to ws.interaad.com.ar with the base path api/sdk/. Ensure your API key has the necessary permissions for agreement management.

Libraries

iws_agreements