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:

dependencies:
  cardscan_client: ^1.0.0

Then run:

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

Basic Usage

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

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

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

3. Upload Image

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

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

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

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

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

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

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:

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

// 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

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

Last updated

Was this helpful?