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
- Check for required agreements on app start
- Use device information for compliance tracking
- Handle network errors gracefully
- Store user acceptance status locally for offline access
- Show agreement summaries for version updates
- 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.