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.
API version
The akedly package calls the Akedly V1.0 REST API (https://api.akedly.io/api/v1). V1.0 has a published end date; see the V1.0 REST API page. For a Flutter app on V1.2, see Flutter / Dart.
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
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
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.
httpClientis an optionalhttp.Client, mainly for tests.
- Name
- sendOTP(phoneNumber, email)
- Type
- Future<String>
- Description
Creates and activates a transaction and sends the OTP.
emailis required but nullable: passnullfor a phone-only send. Returns the verification ID forverifyOTP. ThrowsAkedlyExceptionif the request fails.
- Name
- verifyOTP(verificationId, otp)
- Type
- Future<bool>
- Description
Returns
truewhen the API confirms the OTP. Returnsfalsefor a parsed non-200 response or when a 200 response does not confirm it. ThrowsAkedlyExceptionon 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) andstatusCode(int?): the HTTP status when the exception keeps it, otherwisenull.
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.
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
What the package contains
akedly is a Dart package that makes HTTP calls to the Akedly API. It contains no platform-specific code, so it adds no SMS auto-detection, app-to-app verification or UI components.
Configuration
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 getcan fail with a Dart SDK version error. See the installation requirements. Upgrade to a Flutter release that bundles Dart 3.2 or newer.
