saafe_aa_sdk 1.0.1 copy "saafe_aa_sdk: ^1.0.1" to clipboard
saafe_aa_sdk: ^1.0.1 copied to clipboard

Flutter SDK for integrating SAAFE's secure authentication and financial experience

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'dart:io' show Platform;
import 'package:saafe_aa_sdk/saafe_sdk.dart';
import 'package:http/http.dart' as http;

void main() async {
  // Initialize Flutter binding
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize SAAFE SDK with sandbox mode (default is true)
  await SaafeSdk.initialize(useSandbox: true);
  
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SAAFE SDK Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const SaafeDemo(),
    );
  }
}


class SaafeDemo extends StatefulWidget {
  const SaafeDemo({Key? key}) : super(key: key);

  @override
  State<SaafeDemo> createState() => SaafeDemoState();
}

class SaafeDemoState extends State<SaafeDemo> {
  String fi = '';
  String reqdate = '';
  String ecreq = '';

  final TextEditingController textEditingController = TextEditingController();
  final FocusNode focusNode = FocusNode();

  SaafeRedirectInstance? _redirectInstance;
  String _status = 'Not started';
  bool _isSupported = true;
  bool _useSandbox = true; // Default to sandbox environment
  bool _showCloseButton = true; // Default to show close button
  DisplayMode _displayMode = DisplayMode.bottomSheet; // Default to bottom sheet

  @override
  void initState() {
    super.initState();
    _checkPlatformSupport();
    setState(() {
      _status = 'Enter mobile number and click "Start SAAFE Flow"';
    });
    // Don't fetch data on init - wait for user to enter mobile number
  }

  void _checkPlatformSupport() {
    setState(() {
      _isSupported = !(kIsWeb || !(Platform.isAndroid || Platform.isIOS));
      if (!_isSupported) {
        _status = 'Platform not supported';
      }
    });
  }

  // Switch between sandbox and production environments
  Future<void> _toggleEnvironment() async {
    setState(() {
      _useSandbox = !_useSandbox;
      // Re-initialize the SDK with the new environment setting
      SaafeSdk.initialize(useSandbox: _useSandbox);
      _status = 'Switched to ${_useSandbox ? 'sandbox' : 'production'} environment';
    });
  }

  bool get isTenDigits => textEditingController.text.length == 10;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('SAAFE SDK Demo'),
        actions: [
          // Add environment indicator and toggle button in the app bar
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16.0),
            child: Center(
              child: Row(
                children: [
                  // Environment indicator text
                  Text(
                    _useSandbox ? 'SANDBOX' : 'PRODUCTION',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      color: _useSandbox ? Colors.amber : Colors.red,
                    ),
                  ),
                  // Toggle button
                  Switch(
                    value: _useSandbox,
                    activeColor: Colors.amber,
                    inactiveTrackColor: Colors.red.withOpacity(0.5),
                    onChanged: (value) => _toggleEnvironment(),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
      body: Center(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'Status: $_status',
                style: const TextStyle(fontSize: 18),
              ),
              const SizedBox(height: 30),
              if (!_isSupported)
                const Text(
                  'This platform does not support WebView. SAAFE SDK requires Android or iOS.',
                  textAlign: TextAlign.center,
                  style: TextStyle(color: Colors.red, fontSize: 16),
                )
              else ...[
                // Environment selection card
                Card(
                  elevation: 2,
                  margin: const EdgeInsets.only(bottom: 20),
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Text(
                          'Environment',
                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                        ),
                        const SizedBox(height: 8),
                        Row(
                          children: [
                            Switch(
                              value: _useSandbox,
                              activeColor: Colors.green,
                              onChanged: (value) => _toggleEnvironment(),
                            ),
                            const SizedBox(width: 8),
                            Text(
                              _useSandbox ? 'Sandbox' : 'Production',
                              style: TextStyle(
                                fontWeight: FontWeight.bold,
                                color: _useSandbox ? Colors.green : Colors.red,
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 4),
                        Text(
                          _useSandbox 
                              ? 'Using stage-redirection.saafe.in'
                              : 'Using app-redirection.saafe.in',
                          style: TextStyle(
                            fontSize: 12,
                            color: Colors.grey[600],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
                
                // Display mode selection card
                Card(
                  elevation: 2,
                  margin: const EdgeInsets.only(bottom: 20),
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Text(
                          'Display Mode',
                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                        ),
                        const SizedBox(height: 8),
                        Row(
                          children: [
                            Radio<DisplayMode>(
                              value: DisplayMode.bottomSheet,
                              groupValue: _displayMode,
                              onChanged: (DisplayMode? value) {
                                setState(() {
                                  _displayMode = value!;
                                });
                              },
                            ),
                            const Text('Bottom Sheet'),
                            const SizedBox(width: 20),
                            Radio<DisplayMode>(
                              value: DisplayMode.fullPage,
                              groupValue: _displayMode,
                              onChanged: (DisplayMode? value) {
                                setState(() {
                                  _displayMode = value!;
                                });
                              },
                            ),
                            const Text('Full Page'),
                          ],
                        ),
                        const SizedBox(height: 8),
                        Row(
                          children: [
                            Checkbox(
                              value: _showCloseButton,
                              onChanged: (bool? value) {
                                setState(() {
                                  _showCloseButton = value!;
                                });
                              },
                            ),
                            Expanded(
                              child: Text(
                                _displayMode == DisplayMode.fullPage 
                                  ? 'Show Close Button (Optional for full page)'
                                  : 'Show Close Button',
                              ),
                            ),
                          ],
                        ),
                        if (_displayMode == DisplayMode.fullPage)
                          Padding(
                            padding: const EdgeInsets.only(top: 4.0, left: 40.0),
                            child: Text(
                              'In full page mode, users can use system back button to exit',
                              style: TextStyle(
                                fontSize: 12,
                                color: Colors.grey[600],
                                fontStyle: FontStyle.italic,
                              ),
                            ),
                          ),
                        if (_displayMode == DisplayMode.bottomSheet)
                          Padding(
                            padding: const EdgeInsets.only(top: 4.0, left: 40.0),
                            child: Text(
                              'In bottom sheet mode, users can drag down, use close button, or system back button',
                              style: TextStyle(
                                fontSize: 12,
                                color: Colors.grey[600],
                                fontStyle: FontStyle.italic,
                              ),
                            ),
                          ),
                      ],
                    ),
                  ),
                ),
                
                TextField(
                  controller: textEditingController,
                  maxLength: 10,
                  keyboardType: TextInputType.phone,
                  decoration: InputDecoration(
                    labelText: 'Mobile Number',
                    hintText: 'Enter 10 digit mobile number',
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(12),
                    ),
                    prefixIcon: const Icon(Icons.phone),
                    filled: true,
                    fillColor: Colors.grey[100],
                    counterText: '', // Hide character counter
                  ),
                  onChanged: (value) {
                    setState(() {
                      // Update UI when text changes
                    });
                  },
                ),
                const SizedBox(height: 20),
                ElevatedButton(
                  onPressed: isTenDigits ? _fetchAndStartFlow : null,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.blueAccent,
                    minimumSize: const Size(200, 50),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(12),
                    ),
                  ),
                  child: const Text('Start SAAFE Flow', style: TextStyle(color: Colors.white)),
                ),
                if (_redirectInstance != null)
                  Padding(
                    padding: const EdgeInsets.only(top: 16.0),
                    child: ElevatedButton(
                      onPressed: _closeFlow,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.redAccent,
                        minimumSize: const Size(200, 50),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                      child: const Text('Close Flow',style: TextStyle(color: Colors.white),),
                    ),
                  ),
              ],
            ],
          ),
        ),
      ),
    );
  }

  Future<void> _fetchAndStartFlow() async {
    setState(() {
      _status = 'Fetching data...';
    });

    try {
      // Get mobile number from text field
      final mobileNumber = textEditingController.text;

      // Step 1: Login to get JWT token
      setState(() {
        _status = 'Authenticating...';
      });

      const loginUrl = 'https://saafe-tsp-api.saafe.tech/api/login';
      final loginResponse = await http.post(
        Uri.parse(loginUrl),
        headers: {
          'Content-Type': 'application/json',
          'accept': 'application/json, text/plain, */*',
          'accept-language': 'en-US,en;q=0.9',
          'origin': 'https://dev.tsp.saafe.tech',
          'referer': 'https://dev.tsp.saafe.tech/',
          'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
        },
        body: jsonEncode({
          "email": "tspdev@saafe.tech",
          "password": "Sudharsansk99@"
        }),
      );

      print("Login API Response Status: ${loginResponse.statusCode}");
      if (kDebugMode) {
        print("Login API Response Body: ${loginResponse.body}");
      }

      if (loginResponse.statusCode != 200) {
        setState(() {
          _status = 'Login Error: ${loginResponse.statusCode}';
        });
        return;
      }

      final loginData = jsonDecode(loginResponse.body);
      final String jwtToken = loginData['data']?['access_token'] ?? 
                              loginData['access_token'] ?? 
                              loginData['token'] ?? '';
      
      if (jwtToken.isEmpty) {
        setState(() {
          _status = 'Login Error: No token received';
        });
        return;
      }

      if (kDebugMode) {
        print("JWT Token received: ${jwtToken.substring(0, 20)}...");
      }

      // Step 2: Generate consent using the JWT token
      setState(() {
        _status = 'Generating consent...';
      });

      const consentUrl = 'https://saafe-tsp-api.saafe.tech/api/consent';
      final consentResponse = await http.post(
        Uri.parse(consentUrl),
        headers: {
          'Content-Type': 'application/json',
          'accept': 'application/json, text/plain, */*',
          'accept-language': 'en-US,en;q=0.9',
          'authorization': 'Bearer $jwtToken',
          'origin': 'https://dev.tsp.saafe.tech',
          'referer': 'https://dev.tsp.saafe.tech/',
          'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
        },
        body: jsonEncode({
          "fair_use_id": "CT008",
          "aa_id": ["SAAFE-UAT"],
          "consent_types": ["PROFILE", "SUMMARY", "TRANSACTIONS"],
          "consent_start_from": "2025-06-16T18:30:00.000Z",
          "consent_expiry": "2026-06-17T18:29:59.999Z",
          "customer_id": null,
          "mobile_number": mobileNumber,
          "email": "shidiqadm@gmail.com",
          "fetch_type": ["PERIODIC"],
          "fi_types": [
            "DEPOSIT", "TERM_DEPOSIT", "RECURRING_DEPOSIT", "SIP", "CP",
            "GOVT_SECURITIES", "EQUITIES", "BONDS", "DEBENTURES", "ETF",
            "IDR", "CIS", "AIF", "INVIT", "REIT", "GSTR1_3B", "MUTUAL_FUNDS"
          ],
          "data_life_unit": "MONTH",
          "data_life_value": 1,
          "frequency_unit": "MONTH",
          "frequency_value": 45,
          "purpose_code": "102",
          "purpose_url": "https://api.rebit.org.in/aa/purpose/102.xml",
          "purpose_description": "Customer spending patterns, budget or other reportings",
          "purpose_category": "01",
          "save_as_template": false,
          "pan_number": null,
          "date_of_birth": null,
          "delivery_mode": ["url", "sms"],
          "fip_id": [],
          "consent_mode": "STORE",
          "fi_datarange_from": "2015-06-16T18:30:00.000Z",
          "fi_datarange_to": "2025-06-17T18:29:59.999Z"
        }),
      );

      print("Consent API Response Status: ${consentResponse.statusCode}");
      if (kDebugMode) {
        print("Consent API Response Body: ${consentResponse.body}");
        print("Using mobile number: $mobileNumber");
      }

      if (consentResponse.statusCode == 200 || consentResponse.statusCode == 201) {
        final jsonResponse = jsonDecode(consentResponse.body);
        if (kDebugMode) {
          print("Parsed JSON Response: $jsonResponse");
        }

        // Check if the API call was actually successful
        final bool isSuccess = jsonResponse['success'] == true;
        final String status = jsonResponse['status'] ?? '';
        
        if (!isSuccess || status == 'error') {
          setState(() {
            _status = 'Consent API Error: ${jsonResponse['message'] ?? 'Unknown error'}';
          });
          if (kDebugMode) {
            print('API returned error status: $status');
            print('Error message: ${jsonResponse['message']}');
          }
          return;
        }

        // Extract parameters from the new API response structure
        // The response structure might be different, so we need to adapt
        setState(() {
          // Try to extract from different possible response structures
          fi = jsonResponse['data']?['encode']?['fi'] ?? 
               jsonResponse['fi'] ?? 
               jsonResponse['data']?['fi'] ?? 
               jsonResponse['consent_handle'] ?? 
               jsonResponse['consentHandle'] ?? '';
          
          reqdate = jsonResponse['data']?['encode']?['reqdate'] ?? 
                   jsonResponse['reqdate'] ?? 
                   jsonResponse['data']?['reqdate'] ?? 
                   jsonResponse['data']?['date'] ?? 
                   jsonResponse['timestamp'] ?? 
                   DateTime.now().millisecondsSinceEpoch.toString();
          
          ecreq = jsonResponse['data']?['encode']?['ecreq'] ?? 
                 jsonResponse['ecreq'] ?? 
                 jsonResponse['data']?['ecreq'] ?? 
                 jsonResponse['encrypted_consent'] ?? 
                 jsonResponse['consent_data'] ?? '';
        });

        print('Extracted values:\nfi: $fi\nreqdate: $reqdate\necreq: $ecreq');
        
        // Check if we have valid parameters from the API
        if (fi.isEmpty || reqdate.isEmpty || ecreq.isEmpty) {
          setState(() {
            _status = 'API Error: Missing parameters from response. Please check the API response structure.';
          });
          if (kDebugMode) {
            print('Response structure: ${jsonResponse.keys.toList()}');
          }
          return;
        }
        
        // Now start the SAAFE flow with the fetched parameters
        _startSaafeFlow();
      } else {
        if (kDebugMode) {
          print('Failed to generate consent. Status code: ${consentResponse.statusCode}');
        }
        setState(() {
          _status = 'Consent API Error: ${consentResponse.statusCode}';
        });
      }
    } catch (e) {
      if (kDebugMode) {
        print('Error in API calls: $e');
      }
      setState(() {
        _status = 'API Error: $e';
      });
    }
  }

  Future<void> _startSaafeFlow() async {
    setState(() {
      _status = 'Starting...';
    });

    if (kDebugMode) {
      print("Starting SAAFE flow with: fi=$fi, reqdate=$reqdate, ecreq=$ecreq");
      print("Display mode: $_displayMode, Show close button: $_showCloseButton");
      print("Timestamp of parameters: ${DateTime.now()}");
    }
    
    final options = SaafeRedirectOptions(
      fi: fi,
      reqdate: reqdate,
      ecreq: ecreq,
      showCloseButton: _showCloseButton,
      displayMode: _displayMode,
      onLoad: () {
        if (kDebugMode) {
          print("WebView onLoad callback fired");
        }
        setState(() {
          _status = 'Flow loaded';
        });
      },
      onComplete: (data) {
        if (kDebugMode) {
          print("WebView onComplete callback fired with data: $data");
        }
        setState(() {
          _status = 'Completed successfully! Data: ${data.toString()}';
          _redirectInstance = null;
        });
        
        // Client app decides when to close the SDK
        // Navigator.of(context).pop(); // Close the SDK
        
        // Here you can navigate to a specific screen based on the completion data
        // For example: Navigator.pushNamed(context, '/success-screen');
      },
      onCancel: () {
        if (kDebugMode) {
          print("WebView onCancel callback fired");
        }
        setState(() {
          _status = 'Cancelled by user';
          _redirectInstance = null;
        });
        
        // Client app decides when to close the SDK
        Navigator.of(context).pop(); // Close the SDK
        
        // Here you can navigate to a specific screen or just stay on current screen
      },
      onError: (error) {
        if (kDebugMode) {
          print("WebView onError callback fired with error: $error");
        }
        setState(() {
          _status = 'Error: $error';
          _redirectInstance = null;
        });
        
        // Client app decides when to close the SDK
        // Navigator.of(context).pop(); // Close the SDK
        
        // Here you can show an error dialog or navigate to an error screen
        // For example: _showErrorDialog(error);
      },
    );

    _redirectInstance = await SaafeSdk.triggerRedirect(context, options);
  }

  void _closeFlow() {
    _redirectInstance?.close();
    setState(() {
      _status = 'Closed by app';
      _redirectInstance = null;
    });
  }
}
2
likes
130
points
301
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for integrating SAAFE's secure authentication and financial experience

Homepage
Repository (GitHub)

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

cupertino_icons, flutter, http, url_launcher, webview_flutter

More

Packages that depend on saafe_aa_sdk