Flutter SDK
The Akedly Flutter SDK provides a streamlined Dart interface for OTP authentication in Flutter applications across all platforms. With automatic transaction management and robust error handling, it's the fastest way to add authentication to your Flutter app.
Installation
Add the Akedly package to your pubspec.yaml
file and install dependencies.
Installation
dependencies:
flutter:
sdk: flutter
akedly: ^0.0.3
Install
flutter pub get
Requirements
- Flutter 2.0 or higher
- Dart 2.12 or higher
- Akedly account with API key and pipeline ID
Quick Start
Initialize the Akedly client with your credentials and use the simplified two-method API.
Setup
import 'package:akedly/akedly.dart';
final akedlyClient = AkedlyClient(
apiKey: 'your_api_key_here',
pipelineId: 'your_pipeline_id_here',
);
Authentication Flow
The Flutter SDK provides an even simpler interface with automatic transaction management:
Send OTP: Creates, activates transaction and sends OTP in one call Verify OTP: Verifies the user's input using the returned verification ID
Authentication Flow
class AuthService {
final AkedlyClient _akedly = AkedlyClient(
apiKey: 'your_api_key_here',
pipelineId: 'your_pipeline_id_here',
);
Future<String?> sendOTP(String phoneNumber) async {
try {
// Sends OTP and returns verification ID
final verificationId = await _akedly.sendOTP(phoneNumber);
return verificationId;
} catch (e) {
print('Failed to send OTP: $e');
return null;
}
}
Future<bool> verifyOTP(String verificationId, String otp) async {
try {
final isValid = await _akedly.verifyOTP(verificationId, otp);
return isValid;
} catch (e) {
print('OTP verification failed: $e');
return false;
}
}
}
Complete Widget Example
Flutter Widget
import 'package:flutter/material.dart';
import 'package:akedly/akedly.dart';
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();
super.dispose();
}
Future<void> _sendOTP() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final verificationId = await _akedly.sendOTP(_phoneController.text.trim());
if (verificationId != null) {
setState(() {
_verificationId = verificationId;
_otpSent = true;
_resendCountdown = 60;
});
_startResendCountdown();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('OTP sent successfully!'),
backgroundColor: Colors.green,
),
);
}
} else {
setState(() {
_errorMessage = 'Failed to send OTP. Please try again.';
});
}
} catch (e) {
setState(() {
_errorMessage = 'Error: ${e.toString()}';
});
} finally {
setState(() => _isLoading = false);
}
}
Future<void> _verifyOTP() async {
if (_verificationId == null || _otpController.text.trim().length < 6) return;
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final isValid = await _akedly.verifyOTP(
_verificationId!,
_otpController.text.trim(),
);
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();
});
}
} catch (e) {
setState(() {
_errorMessage = 'Verification failed: ${e.toString()}';
});
} finally {
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: 6,
onChanged: (value) {
if (value.length == 6) {
_verifyOTP();
}
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading || _otpController.text.length < 6
? 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
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)
- Type
- constructor
- Description
Initialize the client with your Akedly credentials from the dashboard
- Name
sendOTP(phoneNumber)
- Type
- Future<String?>
- Description
Send OTP to phone number. Returns verification ID for the next step, or null if failed
- Name
verifyOTP(verificationId, otp)
- Type
- Future<bool>
- Description
Verify the OTP using verification ID and user input. Returns true if valid, false otherwise
Error Handling
The SDK handles all common errors automatically, but you should still implement proper error handling:
Error Handling
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);
if (verificationId != null) {
return AuthResult.success(data: verificationId);
} else {
return AuthResult.failure(message: 'Failed to send OTP');
}
} on AkedlyException catch (e) {
// Handle specific Akedly errors
switch (e.code) {
case 'RATE_LIMITED':
return AuthResult.failure(
message: 'Too many attempts. Please wait ${e.retryAfter} seconds.',
retryAfter: e.retryAfter,
);
case 'INVALID_PHONE':
return AuthResult.failure(message: 'Invalid phone number format');
case 'DELIVERY_FAILED':
return AuthResult.failure(message: 'Failed to deliver OTP. Please check your phone number.');
default:
return AuthResult.failure(message: e.message);
}
} catch (e) {
return AuthResult.failure(message: 'Network error: ${e.toString()}');
}
}
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.');
}
} catch (e) {
return AuthResult.failure(message: 'Verification failed: ${e.toString()}');
}
}
}
class AuthResult {
final bool isSuccess;
final String? message;
final String? data;
final int? retryAfter;
AuthResult.success({this.data})
: isSuccess = true, message = null, retryAfter = null;
AuthResult.failure({required this.message, this.retryAfter})
: isSuccess = false, data = null;
}
Platform Support
Cross-Platform: The Flutter SDK supports all Flutter platforms including Android, iOS, Linux, macOS, Web, and Windows with automatic transaction management and robust error handling.
Platform-Specific Features
- Name
Android & iOS
- Type
- platforms
- Description
Full support with automatic SMS detection and app-to-app verification flows
- Name
Web
- Type
- platforms
- Description
Complete functionality with responsive UI components optimized for web browsers
- Name
Desktop (macOS, Linux, Windows)
- Type
- platforms
- Description
Full API access with desktop-optimized UI patterns and keyboard navigation
Configuration
Configuration
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
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.
Debug Mode
Debug Configuration
final akedlyClient = AkedlyClient(
apiKey: 'your_api_key_here',
pipelineId: 'your_pipeline_id_here',
debugMode: true, // Enables detailed logging
);