Skip to content
Akedly
Legacy

Flutter SDK

The akedly package is a small Dart client for the Akedly API. sendOTP creates and activates a transaction in one call, and verifyOTP checks the code the user enters.

Installation

Add the akedly package to your pubspec.yaml file and install dependencies. ^0.0.4 is Dart's caret range for a 0.0.x version and accepts >=0.0.4 <0.1.0. The code on this page is written for 0.0.4.

Requirements

  • An Akedly account with an API key and pipeline ID
  • Dart 3.2 or newer (see below)

akedly 0.0.4 declares Dart >=2.12.0 <4.0.0 and Flutter >=1.17.0 in its own pubspec.yaml. Those are the package's declared constraints, not a tested minimum. The package depends on http ^1.2.0, and http 1.2.0 requires Dart ^3.2.0. A project that resolves akedly 0.0.4 therefore needs Dart 3.2 or newer in practice, so use a Flutter release that bundles Dart 3.2 or newer.

Quick Start

Initialize the Akedly client with your credentials and use the simplified two-method API.

Authentication Flow

The package manages the transaction for you:

Send OTP: Creates and activates a transaction, sends the OTP, and returns the verification ID

Verify OTP: Checks the user's input against that verification ID

Complete Widget Example

Flutter Widget

DARTlib/widgets/otp_auth_widget.dart
import 'package:flutter/material.dart';
import 'package:akedly/akedly.dart';

// This SDK's send methods don't take `digits`, so Akedly sends its default 6-digit
// code; set this to match the length your transactions actually use (4, 5 or 6).
// Changing this constant alone does NOT change the code Akedly sends.
const otpLength = 6;

class OTPAuthWidget extends StatefulWidget {
  final Function(bool) onAuthenticationComplete;

  const OTPAuthWidget({
    Key? key,
    required this.onAuthenticationComplete,
  }) : super(key: key);

  @override
  _OTPAuthWidgetState createState() => _OTPAuthWidgetState();
}

class _OTPAuthWidgetState extends State<OTPAuthWidget> {
  final _phoneController = TextEditingController();
  final _otpController = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  
  late final AkedlyClient _akedly;
  
  String? _verificationId;
  bool _isLoading = false;
  bool _otpSent = false;
  String? _errorMessage;
  int _resendCountdown = 0;

  @override
  void initState() {
    super.initState();
    _akedly = AkedlyClient(
      apiKey: 'your_api_key_here',
      pipelineId: 'your_pipeline_id_here',
    );
  }

  @override
  void dispose() {
    _phoneController.dispose();
    _otpController.dispose();
    _akedly.dispose();
    super.dispose();
  }

  Future<void> _sendOTP() async {
    if (!_formKey.currentState!.validate()) return;
    
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });
    
    try {
      // The second argument is the optional email; pass null for a phone-only send.
      final verificationId = await _akedly.sendOTP(
        _phoneController.text.trim(),
        null,
      );
      if (!mounted) return;

      setState(() {
        _verificationId = verificationId;
        _otpSent = true;
        _resendCountdown = 60;
      });

      _startResendCountdown();

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('OTP sent successfully!'),
            backgroundColor: Colors.green,
          ),
        );
      }
    } on AkedlyException catch (e) {
      if (!mounted) return;
      setState(() {
        _errorMessage = e.message;
      });
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  Future<void> _verifyOTP() async {
    if (_verificationId == null || _otpController.text.trim().length < otpLength) return;
    
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });
    
    try {
      final isValid = await _akedly.verifyOTP(
        _verificationId!, 
        _otpController.text.trim(),
      );
      if (!mounted) return;
      
      if (isValid) {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('Authentication successful!'),
              backgroundColor: Colors.green,
            ),
          );
        }
        widget.onAuthenticationComplete(true);
      } else {
        setState(() {
          _errorMessage = 'Invalid OTP. Please try again.';
          _otpController.clear();
        });
      }
    } on AkedlyException catch (e) {
      if (!mounted) return;
      setState(() {
        _errorMessage = 'Verification failed: ${e.message}';
      });
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  void _startResendCountdown() {
    Future.delayed(const Duration(seconds: 1), () {
      if (mounted && _resendCountdown > 0) {
        setState(() => _resendCountdown--);
        _startResendCountdown();
      }
    });
  }

  void _resetForm() {
    setState(() {
      _otpSent = false;
      _verificationId = null;
      _errorMessage = null;
      _resendCountdown = 0;
    });
    _otpController.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            if (!_otpSent) ...[
              // Phone number input screen
              Text(
                'Enter your phone number',
                style: Theme.of(context).textTheme.headlineSmall,
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 24),
              TextFormField(
                controller: _phoneController,
                decoration: const InputDecoration(
                  labelText: 'Phone Number',
                  hintText: '+1234567890',
                  prefixIcon: Icon(Icons.phone),
                  border: OutlineInputBorder(),
                ),
                keyboardType: TextInputType.phone,
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Please enter your phone number';
                  }
                  if (!value.trim().startsWith('+')) {
                    return 'Phone number must include country code (e.g., +1234567890)';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: _isLoading ? null : _sendOTP,
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                ),
                child: _isLoading 
                  ? const SizedBox(
                      height: 20,
                      width: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Text('Send OTP', style: TextStyle(fontSize: 16)),
              ),
            ] else ...[
              // OTP verification screen
              Text(
                'Enter verification code',
                style: Theme.of(context).textTheme.headlineSmall,
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 8),
              Text(
                'Code sent to ${_phoneController.text}',
                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                  color: Colors.grey[600],
                ),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 24),
              TextFormField(
                controller: _otpController,
                decoration: const InputDecoration(
                  labelText: 'Enter OTP',
                  hintText: '123456',
                  prefixIcon: Icon(Icons.security),
                  border: OutlineInputBorder(),
                ),
                keyboardType: TextInputType.number,
                textAlign: TextAlign.center,
                style: const TextStyle(
                  fontSize: 18,
                  letterSpacing: 4,
                  fontWeight: FontWeight.bold,
                ),
                maxLength: otpLength,
                onChanged: (value) {
                  if (value.length == otpLength) {
                    _verifyOTP();
                  }
                },
              ),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: _isLoading || _otpController.text.length < otpLength 
                  ? null 
                  : _verifyOTP,
                style: ElevatedButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                ),
                child: _isLoading 
                  ? const SizedBox(
                      height: 20,
                      width: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Text('Verify OTP', style: TextStyle(fontSize: 16)),
              ),
              const SizedBox(height: 16),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  TextButton(
                    onPressed: _resetForm,
                    child: const Text('Change Phone Number'),
                  ),
                  TextButton(
                    onPressed: _resendCountdown > 0 ? null : _sendOTP,
                    child: Text(
                      _resendCountdown > 0 
                        ? 'Resend in ${_resendCountdown}s'
                        : 'Resend OTP',
                    ),
                  ),
                ],
              ),
            ],
            
            // Error message display
            if (_errorMessage != null) ...[
              const SizedBox(height: 16),
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Colors.red[50],
                  border: Border.all(color: Colors.red[200]!),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Row(
                  children: [
                    Icon(Icons.error_outline, color: Colors.red[700]),
                    const SizedBox(width: 8),
                    Expanded(
                      child: Text(
                        _errorMessage!,
                        style: TextStyle(color: Colors.red[700]),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

Usage Example

Usage

DARTlib/screens/auth_screen.dart
import 'package:flutter/material.dart';
import 'package:your_app/widgets/otp_auth_widget.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Phone Verification'),
        centerTitle: true,
      ),
      body: OTPAuthWidget(
        onAuthenticationComplete: (success) {
          if (success) {
            // Navigate to authenticated screen
            Navigator.of(context).pushReplacementNamed('/home');
          }
        },
      ),
    );
  }
}

API Methods

  • Name
    AkedlyClient(apiKey, pipelineId, httpClient)
    Type
    constructor
    Description

    Create the client with your API key and pipeline ID from the dashboard. httpClient is an optional http.Client, mainly for tests.

  • Name
    sendOTP(phoneNumber, email)
    Type
    Future<String>
    Description

    Creates and activates a transaction and sends the OTP. email is required but nullable: pass null for a phone-only send. Returns the verification ID for verifyOTP. Throws AkedlyException if the request fails.

  • Name
    verifyOTP(verificationId, otp)
    Type
    Future<bool>
    Description

    Returns true when the API confirms the OTP. Returns false for a parsed non-200 response or when a 200 response does not confirm it. Throws AkedlyException on transport or response-parsing failures.

  • Name
    dispose()
    Type
    void
    Description

    Closes the underlying HTTP client. Call it when you are done with the client.

  • Name
    AkedlyException
    Type
    class
    Description

    Thrown when a call fails. Exposes message (String) and statusCode (int?): the HTTP status when the exception keeps it, otherwise null.

Error Handling

sendOTP throws AkedlyException when sending fails. verifyOTP returns false for a parsed non-200 response or a response that does not confirm the OTP; it throws only on transport or response-parsing failures. AkedlyException carries a message and an optional statusCode. It has no error-code field, so branch on statusCode when present. The SDK sets statusCode in two places: when sendOTP reports a non-200 response, and when either method cannot read the response body as a JSON object, which can also happen on a 200. Every other failure is wrapped with a null statusCode. That covers network errors, and also cases such as a 200 response that lacks the expected fields. A null statusCode therefore does not prove that no response arrived.

Error Handling

DART
import 'package:akedly/akedly.dart';

class AuthService {
  final AkedlyClient _akedly = AkedlyClient(
    apiKey: 'your_api_key_here',
    pipelineId: 'your_pipeline_id_here',
  );

  Future<AuthResult> sendOTP(String phoneNumber) async {
    try {
      final verificationId = await _akedly.sendOTP(phoneNumber, null);
      return AuthResult.success(data: verificationId);
    } on AkedlyException catch (e) {
      // AkedlyException exposes only message and statusCode.
      return AuthResult.failure(message: e.message, statusCode: e.statusCode);
    }
  }

  Future<AuthResult> verifyOTP(String verificationId, String otp) async {
    try {
      final isValid = await _akedly.verifyOTP(verificationId, otp);

      if (isValid) {
        return AuthResult.success();
      } else {
        return AuthResult.failure(message: 'Invalid OTP. Please try again.');
      }
    } on AkedlyException catch (e) {
      return AuthResult.failure(message: e.message, statusCode: e.statusCode);
    }
  }
}

class AuthResult {
  final bool isSuccess;
  final String? message;
  final String? data;
  final int? statusCode;

  AuthResult.success({this.data})
    : isSuccess = true, message = null, statusCode = null;

  AuthResult.failure({required this.message, this.statusCode})
    : isSuccess = false, data = null;
}

Platform Support

Configuration

Configuration

DARTlib/config/akedly_config.dart
import 'package:akedly/akedly.dart';

class AkedlyConfig {
  static const bool _isDevelopment = bool.fromEnvironment('dart.vm.product') == false;
  
  static String get apiKey {
    return _isDevelopment 
      ? 'your_development_api_key' 
      : 'your_production_api_key';
  }
  
  static String get pipelineId {
    return _isDevelopment 
      ? 'your_development_pipeline_id' 
      : 'your_production_pipeline_id';
  }
  
  static AkedlyClient createClient() {
    return AkedlyClient(
      apiKey: apiKey,
      pipelineId: pipelineId,
    );
  }
}

Best Practices

  • Singleton Pattern: Create a single instance of AkedlyClient and reuse it throughout your app
  • Error Boundaries: Implement comprehensive error handling with user-friendly messages
  • Loading States: Always show loading indicators during API calls
  • Input Validation: Validate phone numbers and OTPs before sending to the API
  • Rate Limiting: Handle rate limits gracefully with countdown timers
  • Accessibility: Ensure your OTP input widgets are accessible to screen readers
  • Dispose: Call dispose() when you are done with the client to close its HTTP client

Troubleshooting

Common Issues

  • Name
    Initialization Error
    Type
    error
    Description

    Verify your API key and pipeline ID are correct. Check the Akedly dashboard for the latest credentials.

  • Name
    OTP Not Received
    Type
    error
    Description

    Ensure the phone number includes the country code and is in the correct format (+1234567890).

  • Name
    Verification Timeout
    Type
    error
    Description

    OTPs expire after a set time. Request a new OTP if the user takes too long to enter it.

  • Name
    Network Connectivity
    Type
    error
    Description

    Handle network errors gracefully and provide retry mechanisms for users.

  • Name
    Dependency Resolution Fails
    Type
    error
    Description

    flutter pub get can fail with a Dart SDK version error. See the installation requirements. Upgrade to a Flutter release that bundles Dart 3.2 or newer.