Flutter / Dart Shield SDK

The akedly_shield package provides Proof-of-Work solving and Turnstile captcha for Akedly V1.2 in Flutter and Dart. Supports background computation via Dart Isolates and a built-in Turnstile widget.

Installation

Add the Git dependency to your pubspec.yaml:

dependencies:
  akedly_shield:
    git:
      url: https://github.com/Akedly-Org/akedly-shield-dart.git
      ref: main

Pin to a specific release by passing a tag instead of main:

dependencies:
  akedly_shield:
    git:
      url: https://github.com/Akedly-Org/akedly-shield-dart.git
      ref: v1.0.0

Then fetch the dependency:

flutter pub get

Quick Start

Import the package and use solvePowInIsolate for Flutter apps (recommended -- runs in a background Isolate to keep the UI responsive).

For Turnstile, add the AkedlyTurnstile widget to your widget tree and receive the token via the onToken callback. The PoW solver and Turnstile widget run on the device; only non-sensitive proofs travel to your backend.

Quick Start

DART
// Express proxy — put this on your server, not in the Flutter app.
import express from 'express';

const app = express();
app.use(express.json());
// Optional — only needed if you want per-end-user-IP rate limiting.
// Drop this line and the x-end-user-ip header below if you don't.
// Makes req.ip the real client IP behind a reverse proxy (1 = trust first hop).
app.set('trust proxy', 1);

app.get('/auth/akedly/challenge', async (_req, res) => {
  const r = await fetch(
    `https://api.akedly.io/api/v1.2/transactions/challenge` +
    `?APIKey=${process.env.AKEDLY_API_KEY}` +
    `&pipelineID=${process.env.AKEDLY_PIPELINE_ID}`
  );
  res.status(r.status).json(await r.json());
});

app.post('/auth/akedly/send', async (req, res) => {
  const { phoneNumber, powSolution, turnstileToken } = req.body;
  const r = await fetch('https://api.akedly.io/api/v1.2/transactions/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-end-user-ip': req.ip,
    },
    body: JSON.stringify({
      APIKey: process.env.AKEDLY_API_KEY,
      pipelineID: process.env.AKEDLY_PIPELINE_ID,
      verificationAddress: { phoneNumber },
      powSolution,
      turnstileToken,
    }),
  });
  res.status(r.status).json(await r.json());
});

app.post('/auth/akedly/verify', async (req, res) => {
  const { transactionReqID, otp } = req.body;
  const r = await fetch('https://api.akedly.io/api/v1.2/transactions/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ transactionReqID, otp }),
  });
  res.status(r.status).json(await r.json());
});

API Methods

solvePow(challenge, difficulty)

Asynchronous PoW solver that yields to the event loop every 10,000 iterations. Runs on the main thread.

  • Name
    challenge
    Type
    String
    Description

    64-character hex string from the server challenge response.

  • Name
    difficulty
    Type
    int
    Description

    Number of leading hex zeros required.

Returns Future<int> (the nonce).

solvePowInIsolate(challenge, difficulty)

Executes the PoW solver in a separate Dart Isolate. Recommended for Flutter apps to prevent UI thread blocking.

  • Name
    challenge
    Type
    String
    Description

    64-character hex string from the server challenge response.

  • Name
    difficulty
    Type
    int
    Description

    Number of leading hex zeros required.

Returns Future<int> (the nonce).

AkedlyTurnstile Widget

Flutter widget that loads a Cloudflare Turnstile bridge page via WebView for token generation.

  • Name
    siteKey
    Type
    String (required)
    Description

    Cloudflare Turnstile site key from the challenge response.

  • Name
    onToken
    Type
    Function(String) (required)
    Description

    Callback that receives the Turnstile token when verification completes.

  • Name
    onError
    Type
    Function(String)?
    Description

    Error callback. Called if Turnstile verification fails.

  • Name
    bridgeDomain
    Type
    String?
    Description

    Bridge page domain. Defaults to turnstile.akedly.io.


Complete Example

Full Flutter widget implementing the V1.2 OTP flow against a backend proxy (see the Quick Start for the Node.js backend):

Flutter Widget

DART
lib/otp_screen.dart
import 'package:flutter/material.dart';
import 'package:akedly_shield/akedly_shield.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

// Your backend base URL — keeps APIKey and pipelineID server-side.
const backendUrl = 'https://yourapp.com';

class OTPScreen extends StatefulWidget {
  final String phoneNumber;

  const OTPScreen({
    super.key,
    required this.phoneNumber,
  });

  @override
  State<OTPScreen> createState() => _OTPScreenState();
}

class _OTPScreenState extends State<OTPScreen> {
  String? _transactionReqID;
  String _otp = '';
  bool _loading = false;
  String? _error;
  String? _turnstileSiteKey;
  Map<String, dynamic>? _challengeData;

  Future<void> _sendOTP({String? turnstileToken}) async {
    setState(() { _loading = true; _error = null; });
    try {
      // 1. Get challenge from YOUR backend
      final challengeRes = await http.get(
        Uri.parse('$backendUrl/auth/akedly/challenge'),
      );
      final data = jsonDecode(challengeRes.body)['data'];

      // Check if Turnstile is needed and we don't have a token yet
      if (data['turnstile']?['required'] == true && turnstileToken == null) {
        setState(() {
          _challengeData = data;
          _turnstileSiteKey = data['turnstile']['siteKey'];
          _loading = false;
        });
        return; // Wait for Turnstile token via widget callback
      }

      // 2. Solve PoW in background isolate
      final nonce = await solvePowInIsolate(
        data['challenge'] as String,
        data['difficulty'] as int,
      );

      // 3. Send proof via YOUR backend
      final sendRes = await http.post(
        Uri.parse('$backendUrl/auth/akedly/send'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'phoneNumber': widget.phoneNumber,
          'powSolution': {
            'challengeToken': data['challengeToken'],
            'nonce': nonce,
          },
          if (turnstileToken != null) 'turnstileToken': turnstileToken,
        }),
      );
      final result = jsonDecode(sendRes.body);
      setState(() { _transactionReqID = result['data']['transactionReqID']; });
    } catch (e) {
      setState(() { _error = e.toString(); });
    } finally {
      setState(() { _loading = false; });
    }
  }

  Future<void> _verifyOTP() async {
    setState(() { _loading = true; _error = null; });
    try {
      final res = await http.post(
        Uri.parse('$backendUrl/auth/akedly/verify'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({'transactionReqID': _transactionReqID, 'otp': _otp}),
      );
      final result = jsonDecode(res.body);
      if (result['status'] == 'success') {
        if (mounted) Navigator.of(context).pop(true);
      } else {
        setState(() { _error = 'Invalid OTP'; });
      }
    } catch (e) {
      setState(() { _error = e.toString(); });
    } finally {
      setState(() { _loading = false; });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Verify Phone')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            if (_turnstileSiteKey != null && _transactionReqID == null)
              SizedBox(
                height: 1,
                child: AkedlyTurnstile(
                  siteKey: _turnstileSiteKey!,
                  onToken: (token) => _sendOTP(turnstileToken: token),
                  onError: (err) => setState(() { _error = err; }),
                ),
              ),
            if (_transactionReqID == null) ...[
              Text('Send OTP to ${widget.phoneNumber}'),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: _loading ? null : () => _sendOTP(),
                child: Text(_loading ? 'Sending...' : 'Send OTP'),
              ),
            ] else ...[
              TextField(
                onChanged: (v) => setState(() { _otp = v; }),
                decoration: const InputDecoration(labelText: 'Enter OTP'),
                keyboardType: TextInputType.number,
                maxLength: 6,
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: _loading || _otp.length < 6 ? null : _verifyOTP,
                child: Text(_loading ? 'Verifying...' : 'Verify'),
              ),
            ],
            if (_error != null)
              Padding(
                padding: const EdgeInsets.only(top: 16),
                child: Text(_error!, style: const TextStyle(color: Colors.red)),
              ),
          ],
        ),
      ),
    );
  }
}

Platform Support

The Flutter Shield SDK works on Android, iOS, Web, and Desktop (macOS, Windows, Linux). solvePowInIsolate is available on all platforms. AkedlyTurnstile requires WebView support (Android, iOS, Web).


Was this page helpful?