SAAFE SDK for Flutter

This Flutter SDK provides a simple interface to integrate SAAFE's secure authentication and financial experience into your Flutter applications.

Features

  • 🔒 Secure authentication and redirection flows
  • 📱 Platform detection (iOS, Android, Flutter)
  • 🌓 Advanced theme support (light, dark, system) with automatic status bar styling
  • 📏 Forced portrait orientation during flows
  • 🔗 External links handling with intelligent fallbacks
  • ✅ Confirmation dialogs for closing flows
  • 🖥️ High-resolution WebView with optimized text rendering
  • 🎛️ Customizable display modes (bottom sheet or full page)
  • 🔘 Optional close button visibility control
  • 📞 Enhanced callback handling with structured data for AA flow events
  • 🎯 Client-controlled navigation (SDK doesn't auto-close)

Installation

Add the SDK to your pubspec.yaml:

dependencies:
  saafe_aa_sdk: ^1.0.1

Usage

Initialize the SDK

Initialize the SDK in your main.dart file before calling runApp():

import 'package:saafe_aa_sdk/saafe_sdk.dart';

void main() async {
  // Initialize with default settings (sandbox environment)
  await SaafeSdk.initialize();
  
  // Or specify sandbox/production environment
  // await SaafeSdk.initialize(useSandbox: true); // For sandbox (default)
  // await SaafeSdk.initialize(useSandbox: false); // For production
  
  // Or with a custom redirect URL
  // await SaafeSdk.initialize(customRedirectUrl: 'https://your-custom-url.example.com/login');
  
  runApp(const MyApp());
}

## Enhanced Callback Handling

The SDK now provides structured callback data for AA flow events:

### Success Callbacks (onComplete)

**Consent Approved:**
```json
{
  "type": "AA",
  "status": "approved", 
  "flowCompleted": true,
  "consentHandle": "consent_handle_value",
  "userRating": 5,
  "redirectUrl": "redirect_url",
  "timestamp": "2023-12-01T10:00:00.000Z"
}

Consent Rejected:

{
  "type": "AA",
  "status": "rejected",
  "flowCompleted": true, 
  "consentHandle": "consent_handle_value",
  "rejectionReason": "reason",
  "rejectedByFips": ["fip1", "fip2"],
  "userFeedback": "feedback_text",
  "customFeedback": "custom_feedback_if_any",
  "redirectUrl": "redirect_url",
  "timestamp": "2023-12-01T10:00:00.000Z"
}

Error Callbacks (onError)

{
  "type": "error",
  "status": "error",
  "flowCompleted": false,
  "errorCode": "ERROR_CODE",
  "errorMessage": "Detailed error message",
  "errorType": "network|validation|unknown",
  "statusCode": 400,
  "timestamp": "2023-12-01T10:00:00.000Z",
  "userAgent": "browser_info",
  "url": "current_url",
  "stack": "error_stack_trace",
  "consentHandle": "consent_handle_if_available",
  "redirectUrl": "redirect_url_if_available"
}

Theme Support

The SDK automatically adapts to system theme changes:

Dark Mode (#051923 header background)

  • Status bar icons: White
  • Close button: White
  • Automatic detection of system dark mode

Light Mode (#f9fafb header background)

  • Status bar icons: Dark
  • Close button: Dark
  • Automatic detection of system light mode

The theme is automatically applied based on the device's current theme setting.

Client-Controlled Navigation

⚠️ Important Change in v1.0.1: The SDK no longer automatically closes itself after callbacks. Your app now controls navigation:

final redirectInstance = await SaafeSdk.triggerRedirect(
  context,
  SaafeRedirectOptions(
    fi: fi,
    reqdate: reqdate,
    ecreq: ecreq,
    onComplete: (data) {
      // Parse the completion data
      final responseData = jsonDecode(data);
      
      if (responseData['status'] == 'approved') {
        // Close SDK and navigate to success screen
        Navigator.of(context).pop();
        Navigator.pushNamed(context, '/success');
      } else if (responseData['status'] == 'rejected') {
        // Handle rejection - maybe don't close immediately
        _showRejectionDialog(responseData['rejectionReason']);
      }
    },
    onError: (error) {
      // Parse error details
      final errorData = jsonDecode(error);
      
      // Decide whether to close SDK or show retry option
      if (errorData['errorType'] == 'network') {
        _showRetryDialog(); // Keep SDK open for retry
      } else {
        Navigator.of(context).pop(); // Close SDK for other errors
        _showErrorScreen();
      }
    },
    onCancel: () {
      // User cancelled - close SDK
      Navigator.of(context).pop();
    },
  ),
);

Environment Configuration

The SDK supports two environments:

  1. Sandbox (default): Uses https://stage-redirection.saafe.in/login - For development and testing
  2. Production: Uses https://app-redirection.saafe.in/login - For live applications

To switch environments, use the useSandbox parameter during initialization:

// For sandbox (development/testing)
await SaafeSdk.initialize(useSandbox: true);

// For production (live environment)
await SaafeSdk.initialize(useSandbox: false);

Display Modes

The SDK supports two display modes for the WebView:

  1. Bottom Sheet (default): Shows the WebView in a modal bottom sheet
  2. Full Page: Shows the WebView as a full page with left-to-right transition
// Bottom sheet mode (default)
final redirectInstance = await SaafeSdk.triggerRedirect(
  context,
  SaafeRedirectOptions(
    // ... other parameters
    displayMode: DisplayMode.bottomSheet,
  ),
);

// Full page mode
final redirectInstance = await SaafeSdk.triggerRedirect(
  context,
  SaafeRedirectOptions(
    // ... other parameters
    displayMode: DisplayMode.fullPage,
  ),
);

Close Button Visibility

You can control whether the close button is shown in the WebView:

final redirectInstance = await SaafeSdk.triggerRedirect(
  context,
  SaafeRedirectOptions(
    // ... other parameters
    showCloseButton: true,  // Show close button (default)
    // showCloseButton: false, // Hide close button
  ),
);

Note: In full page mode, the close button is optional since users can use the system back button to exit. In bottom sheet mode, the close button provides an additional way to close the modal alongside the drag-to-dismiss gesture.

Both display modes support system back navigation with confirmation dialog to prevent accidental exits.

The SDK will automatically trigger the onComplete callback with the received data.

Trigger the Redirect Flow

Trigger the redirect flow using the parameters from your generated consent response:

// First, get the consent response from SAAFE API
// The response will look like this:
/*
{
    "status": "success",
    "code": "OperationSuccess",
    "msg": "Operation Success",
    "request_id": 7788,
    "txn_id": "daa53abbc1-7fe7-4984-8879-197883daba9d",
    "url": "https://sandbox.redirection.saafe.in/login/?fi=WU9AUEBGV0dXUB5QWUc=&reqdate=160520250536024&ecreq=PuH3d0DSVsNCH3tfu86D4iNKTOYZyyftje7GLsc84gwMcmjFifpO9Pf4v8gDZ0lN4pD7Koskq-CkrHFGRpkvh16HVVjCjmhHiQaKRGuRo1MjoxiqicJJcQP0k-MJwDYR6krtMHFr3RP_W4Trlj2aAxeoucAvoJhGI1Lm5fnsCnnU64BHl7vPHGyXovHZIXl4sr8_HLQPuBkgLUy5oeu0fDDY62ajnztF40KufzuNe5MLI0Aut2_OjR-S94rdFrkbM6IseaBsPbpZnnibU8GuKL3A9idlvBDbLRNvEtgnjKy6AKGMAWQtEC1l6waJKeedhl0j4qCpNLtNBOCXb3VS5sf5kafDitfuSgpJ0pfuA-8=",
    "consent_handle": [
        "8bce1c3b-f289-4c1c-8d4b-7ed2e0d3a31b",
        "b6bc69f8-c77d-4070-a9ec-0d4ad7c72be2"
    ],
    "vua": "8682807087@dashboard-aa-preprod",
    "fi": "WU9AUEBGV0dXUB5QWUc=",
    "reqdate": "160520250536024",
    "ecreq": "PuH3d0DSVsNCH3tfu86D4iNKTOYZyyftje7GLsc84gwMcmjFifpO9Pf4v8gDZ0lN4pD7Koskq-CkrHFGRpkvh16HVVjCjmhHiQaKRGuRo1MjoxiqicJJcQP0k-MJwDYR6krtMHFr3RP_W4Trlj2aAxeoucAvoJhGI1Lm5fnsCnnU64BHl7vPHGyXovHZIXl4sr8_HLQPuBkgLUy5oeu0fDDY62ajnztF40KufzuNe5MLI0Aut2_OjR-S94rdFrkbM6IseaBsPbpZnnibU8GuKL3A9idlvBDbLRNvEtgnjKy6AKGMAWQtEC1l6waJKeedhl0j4qCpNLtNBOCXb3VS5sf5kafDitfuSgpJ0pfuA-8="
}
*/

// Extract the required parameters from the consent response
final consentResponse = await yourApiCallToGetConsent();

// Now use these parameters with the SDK
final redirectInstance = await SaafeSdk.triggerRedirect(
  context,
  SaafeRedirectOptions(
    fi: consentResponse['fi'],          // Use the 'fi' value from consent response
    reqdate: consentResponse['reqdate'], // Use the 'reqdate' value from consent response
    ecreq: consentResponse['ecreq'],    // Use the 'ecreq' value from consent response
    theme: 'light', // Optional: 'light', 'dark', or null for system default
    displayMode: DisplayMode.bottomSheet, // Optional: DisplayMode.bottomSheet (default) or DisplayMode.fullPage
    showCloseButton: true, // Optional: true (default) to show close button, false to hide
    onComplete: (data) {
      print('Flow completed: $data');
      // Handle successful completion
    },
    onCancel: () {
      print('Flow cancelled by user');
      // Handle user cancellation
    },
    onError: (error) {
      print('Error during flow: $error');
      // Handle errors
    },
    onLoad: () {
      print('WebView loaded successfully');
      // Optional: handle WebView load event
    },
  ),
);

Close the Redirect Flow

You can programmatically close the flow:

// This will show a confirmation dialog before closing
redirectInstance.close();

URL Parameters

The SDK automatically adds the following parameters to the redirect URL:

Platform Detection

  • platform=ios - For iOS devices
  • platform=android - For Android devices
  • platform=flutter - For other Flutter platforms

Theme Support

  • theme=light - Light theme
  • theme=dark - Dark theme

External URL Handling

The SDK intelligently handles external URLs by:

  1. Detecting external links in the WebView
  2. Automatically opening them in the system's default browser
  3. Providing multiple fallback mechanisms if the default method fails
  4. Showing a useful error dialog with copy functionality if all launch attempts fail

You can also use the URL launcher directly in your app:

// Launch a URL in the external browser
await SaafeSdk.launchExternalUrl(
  'https://example.com',
  context: context, // Optional: provide context to show error dialog if needed
);

Example

See the example folder for a complete implementation example.

License

This project is licensed under the MIT License - see the LICENSE file for details.