# Dart 🎯

The official Dart client for the CardScan API provides support for Flutter, web, and server-side Dart applications.

## Installation

Add to your `pubspec.yaml`:

```yaml
dependencies:
  cardscan_client: ^1.0.0
```

Then run:

```bash
flutter pub get
# or for Dart-only projects
dart pub get
```

## Basic Usage

```dart
import 'package:cardscan_client/cardscan_client.dart';

// Initialize with your API key
final apiKey = 'sk_test_cardscan_ai_...';
final client = CardScanApi(apiKey: apiKey);

// Generate a session token for a user
Future<void> authenticate() async {
  final tokenResponse = await client.getAccessToken(
    userId: 'unique-user-123',
  );
  
  final sessionToken = tokenResponse['Token'];
  final identityId = tokenResponse['IdentityId'];
  final sessionId = tokenResponse['session_id'];
  
  // Initialize client with session token for frontend operations
  final userClient = CardScanApi(
    sessionToken: sessionToken,
    live: false, // Use sandbox
  );
}
```

## Card Scanning Workflow

### 1. Create a Card

```dart
Future<CardApiResponse> createCard() async {
  final card = await userClient.createCard(
    CreateCardRequest(
      enableBacksideScan: false,
      enableLivescan: false,
      metadata: {
        'patient_id': '12345',
        'visit_id': 'v-67890',
      },
    ),
  );
  
  print('Card ID: ${card.cardId}');
  print('State: ${card.state}'); // CardState.pending
  
  return card;
}
```

### 2. Generate Upload URL

```dart
Future<GenerateCardUploadUrlResponse> generateUploadUrl(String cardId) async {
  final uploadData = await userClient.generateCardUploadUrl(
    cardId,
    GenerateCardUploadUrlRequest(
      orientation: ScanOrientation.front,
      captureType: ScanCaptureType.manual,
    ),
  );
  
  return uploadData;
}
```

### 3. Upload Image

```dart
import 'dart:io';
import 'package:dio/dio.dart';

Future<void> uploadImage(
  File imageFile,
  GenerateCardUploadUrlResponse uploadData,
) async {
  final dio = Dio();
  
  final formData = FormData();
  
  // Add upload parameters
  uploadData.uploadParameters.forEach((key, value) {
    formData.fields.add(MapEntry(key, value));
  });
  
  // Add image file
  formData.files.add(MapEntry(
    'file',
    await MultipartFile.fromFile(
      imageFile.path,
      filename: 'card.jpg',
    ),
  ));
  
  final response = await dio.post(
    uploadData.uploadUrl,
    data: formData,
  );
  
  if (response.statusCode != 204) {
    throw Exception('Upload failed: ${response.statusCode}');
  }
}
```

### 4. Poll for Results

```dart
Future<CardApiResponse> waitForCompletion(
  String cardId, {
  Duration timeout = const Duration(minutes: 5),
}) async {
  final startTime = DateTime.now();
  
  while (DateTime.now().difference(startTime) < timeout) {
    final card = await userClient.getCard(cardId);
    
    if (card.state == CardState.completed || 
        card.state == CardState.error) {
      return card;
    }
    
    await Future.delayed(const Duration(seconds: 2));
  }
  
  throw TimeoutException('Card processing timed out');
}

// Usage
final completedCard = await waitForCompletion(card.cardId);
if (completedCard.details != null) {
  print('Member ID: ${completedCard.details!.memberId}');
  print('Group Number: ${completedCard.details!.groupNumber}');
}
```

## Error Handling

```dart
import 'package:cardscan_client/cardscan_client.dart';

try {
  final card = await client.getCard('invalid-id');
} on ApiException catch (e) {
  switch (e.statusCode) {
    case 401:
      print('Invalid API key or session token');
      break;
    case 404:
      print('Card not found');
      break;
    case 429:
      print('Rate limit exceeded');
      break;
    default:
      print('API Error: ${e.statusCode} - ${e.message}');
  }
} catch (e) {
  print('Unexpected error: $e');
}
```

## Flutter Integration

### Camera Capture with image\_picker

```dart
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:cardscan_client/cardscan_client.dart';

class CardScannerScreen extends StatefulWidget {
  @override
  _CardScannerScreenState createState() => _CardScannerScreenState();
}

class _CardScannerScreenState extends State<CardScannerScreen> {
  final client = CardScanApi(sessionToken: 'tok_...');
  final picker = ImagePicker();
  
  bool _isLoading = false;
  CardApiResponse? _scannedCard;
  String? _error;
  
  Future<void> _scanCard() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });
    
    try {
      // Capture image
      final image = await picker.pickImage(
        source: ImageSource.camera,
        maxWidth: 1920,
        maxHeight: 1080,
        imageQuality: 90,
      );
      
      if (image == null) {
        setState(() => _isLoading = false);
        return;
      }
      
      // Create card
      final card = await client.createCard(
        CreateCardRequest(enableBacksideScan: false),
      );
      
      // Generate upload URL
      final uploadData = await client.generateCardUploadUrl(
        card.cardId,
        GenerateCardUploadUrlRequest(
          orientation: ScanOrientation.front,
        ),
      );
      
      // Upload image
      await uploadImage(File(image.path), uploadData);
      
      // Wait for results
      final completedCard = await waitForCompletion(card.cardId);
      
      setState(() {
        _scannedCard = completedCard;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Scan Insurance Card')),
      body: Center(
        child: _isLoading
            ? CircularProgressIndicator()
            : _scannedCard != null
                ? _buildCardDetails()
                : _buildScanButton(),
      ),
    );
  }
  
  Widget _buildCardDetails() {
    final details = _scannedCard!.details;
    
    return Card(
      margin: EdgeInsets.all(16),
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('Member ID: ${details?.memberId ?? 'N/A'}'),
            Text('Group: ${details?.groupNumber ?? 'N/A'}'),
            Text('Payer: ${details?.payerName ?? 'N/A'}'),
            if (details?.copayEr != null)
              Text('ER Copay: \$${details!.copayEr}'),
          ],
        ),
      ),
    );
  }
  
  Widget _buildScanButton() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        ElevatedButton.icon(
          onPressed: _scanCard,
          icon: Icon(Icons.camera_alt),
          label: Text('Scan Card'),
        ),
        if (_error != null)
          Padding(
            padding: EdgeInsets.only(top: 16),
            child: Text(
              _error!,
              style: TextStyle(color: Colors.red),
            ),
          ),
      ],
    );
  }
}
```

### WebSocket Support

```dart
import 'package:web_socket_channel/web_socket_channel.dart';
import 'dart:convert';

class CardScanWebSocket {
  final String token;
  WebSocketChannel? _channel;
  
  CardScanWebSocket({required this.token});
  
  Stream<CardWebsocketEvent> connect() {
    _channel = WebSocketChannel.connect(
      Uri.parse('wss://sandbox.cardscan.ai/v1/ws?token=$token'),
    );
    
    return _channel!.stream.map((data) {
      final json = jsonDecode(data);
      return CardWebsocketEvent.fromJson(json);
    });
  }
  
  void disconnect() {
    _channel?.sink.close();
  }
}

// Usage
final websocket = CardScanWebSocket(token: sessionToken);

websocket.connect().listen((event) {
  switch (event.type) {
    case 'card.processing':
      print('Processing started');
      break;
    case 'card.completed':
      print('Card completed: ${event.cardId}');
      break;
    case 'card.error':
      print('Error: ${event.error}');
      break;
  }
});
```

## Eligibility Verification

```dart
Future<EligibilityApiResponse> verifyEligibility(
  CardApiResponse card,
) async {
  final request = CreateEligibilityRequest(
    eligibility: EligibilityRequest(
      provider: ProviderDto(
        firstName: 'John',
        lastName: 'Smith',
        npi: '1234567890',
      ),
      subscriber: SubscriberDto(
        firstName: card.details?.memberName ?? '',
        lastName: card.details?.memberName ?? '',
        dateOfBirth: '1980-01-01',
      ),
    ),
  );
  
  final eligibility = await client.createEligibility(
    card.cardId,
    request,
  );
  
  // Poll for results
  return await waitForEligibilityCompletion(
    eligibility.eligibilityId,
  );
}
```

## State Management with Riverpod

```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cardscan_client/cardscan_client.dart';

// Providers
final cardScanApiProvider = Provider<CardScanApi>((ref) {
  return CardScanApi(sessionToken: 'tok_...');
});

final currentCardProvider = StateNotifierProvider<CardNotifier, AsyncValue<CardApiResponse?>>((ref) {
  return CardNotifier(ref.read(cardScanApiProvider));
});

// State Notifier
class CardNotifier extends StateNotifier<AsyncValue<CardApiResponse?>> {
  final CardScanApi _api;
  
  CardNotifier(this._api) : super(AsyncValue.data(null));
  
  Future<void> scanCard(File imageFile) async {
    state = AsyncValue.loading();
    
    try {
      // Create card
      final card = await _api.createCard(
        CreateCardRequest(enableBacksideScan: false),
      );
      
      // Generate upload URL
      final uploadData = await _api.generateCardUploadUrl(
        card.cardId,
        GenerateCardUploadUrlRequest(
          orientation: ScanOrientation.front,
        ),
      );
      
      // Upload image
      await uploadImage(imageFile, uploadData);
      
      // Wait for results
      final completedCard = await waitForCompletion(card.cardId);
      
      state = AsyncValue.data(completedCard);
    } catch (e, stack) {
      state = AsyncValue.error(e, stack);
    }
  }
}

// Widget
class CardScanView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final cardState = ref.watch(currentCardProvider);
    
    return cardState.when(
      data: (card) => card != null
          ? CardDetailsWidget(card: card)
          : ScanButton(
              onPressed: () => _pickAndScanImage(ref),
            ),
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => ErrorWidget(error),
    );
  }
  
  Future<void> _pickAndScanImage(WidgetRef ref) async {
    final picker = ImagePicker();
    final image = await picker.pickImage(source: ImageSource.camera);
    
    if (image != null) {
      ref.read(currentCardProvider.notifier).scanCard(File(image.path));
    }
  }
}
```

## Web Support

For Flutter Web, use the HTML file input:

```dart
import 'dart:html' as html;
import 'dart:typed_data';

Future<void> uploadImageWeb(
  Uint8List imageBytes,
  GenerateCardUploadUrlResponse uploadData,
) async {
  final formData = html.FormData();
  
  // Add upload parameters
  uploadData.uploadParameters.forEach((key, value) {
    formData.append(key, value);
  });
  
  // Create blob from bytes
  final blob = html.Blob([imageBytes], 'image/jpeg');
  formData.appendBlob('file', blob, 'card.jpg');
  
  // Upload using Fetch API
  final response = await html.HttpRequest.request(
    uploadData.uploadUrl,
    method: 'POST',
    sendData: formData,
  );
  
  if (response.status != 204) {
    throw Exception('Upload failed: ${response.status}');
  }
}
```

## Configuration

```dart
// Custom configuration
final client = CardScanApi(
  apiKey: 'sk_test_cardscan_ai_...',
  baseUrl: 'https://sandbox.cardscan.ai/v1',
  timeout: Duration(seconds: 30),
  retryAttempts: 3,
  retryDelay: Duration(seconds: 1),
);

// With custom HTTP client
import 'package:dio/dio.dart';

final dio = Dio()
  ..interceptors.add(LogInterceptor())
  ..options.headers['X-Custom-Header'] = 'value';

final clientWithCustomHttp = CardScanApi(
  apiKey: apiKey,
  httpClient: dio,
);
```

## Testing

```dart
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';
import 'package:cardscan_client/cardscan_client.dart';

class MockCardScanApi extends Mock implements CardScanApi {}

void main() {
  group('CardScan Tests', () {
    late MockCardScanApi mockApi;
    
    setUp(() {
      mockApi = MockCardScanApi();
    });
    
    test('creates card successfully', () async {
      final expectedCard = CardApiResponse(
        cardId: 'test-id',
        state: CardState.pending,
      );
      
      when(mockApi.createCard(any)).thenAnswer(
        (_) async => expectedCard,
      );
      
      final card = await mockApi.createCard(
        CreateCardRequest(enableBacksideScan: false),
      );
      
      expect(card.cardId, equals('test-id'));
      expect(card.state, equals(CardState.pending));
    });
  });
}
```

## Source Code

View the source code and contribute: [GitHub](https://github.com/CardScan-ai/api-clients/tree/main/clients/cardscan-dart)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.cardscan.ai/api-clients/dart.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
