비트베이크

Implementing Phone Verification in Flutter MVP in 5 Minutes (No Paperwork Required)

2026-03-20T01:04:06.346Z

FLUTTER-MVP-SMS

Implementing Phone Verification in Flutter MVP in 5 Minutes (No Paperwork Required)

The Paperwork Problem Every Developer Hates

You're building a side project. You need SMS verification. You find an SMS API provider, and they ask for... business registration documents? A signed usage certificate? Government-issued ID verification?

For a weekend project, this is a dealbreaker.

Firebase Phone Auth is an option, but setting up a Firebase project, registering SHA-1 keys, configuring Google Cloud Console, and dealing with platform-specific quirks takes more time than building the actual feature.

In this guide, you'll implement complete phone verification in Flutter using just two REST API endpoints — no Firebase, no paperwork, no complex configuration. Copy the code, plug in your API key, and ship.


What You'll Learn

  • Building an SMS verification service class with Dart's http package
  • Creating a polished OTP input UI with pin_code_fields
  • Implementing countdown timers and resend logic
  • Error handling and security best practices for production

Step 1: Project Setup

Add the required packages to your Flutter project:

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0
  pin_code_fields: ^8.0.1
flutter pub get

That's it for dependencies. No Firebase configuration files, no google-services.json, no GoogleService-Info.plist.


Step 2: Build the SMS Auth Service

The entire SMS verification flow requires only two endpoints:

  • POST /send — Send a verification code to a phone number
  • POST /verify — Verify the code the user enters

Here's the complete service class:

// lib/services/auth_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;

class SmsAuthService {
  static const String _baseUrl = 'https://api.easyauth.io/v1';
  final String _apiKey;

  SmsAuthService(this._apiKey);

  /// Send verification code to phone number
  Future> sendCode(String phoneNumber) async {
    final response = await http.post(
      Uri.parse('$_baseUrl/send'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $_apiKey',
      },
      body: jsonEncode({
        'phone_number': phoneNumber,
      }),
    );

    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw SmsAuthException(
        'Failed to send code',
        response.statusCode,
        response.body,
      );
    }
  }

  /// Verify the code entered by user
  Future> verifyCode({
    required String phoneNumber,
    required String code,
  }) async {
    final response = await http.post(
      Uri.parse('$_baseUrl/verify'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $_apiKey',
      },
      body: jsonEncode({
        'phone_number': phoneNumber,
        'code': code,
      }),
    );

    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw SmsAuthException(
        'Verification failed',
        response.statusCode,
        response.body,
      );
    }
  }
}

class SmsAuthException implements Exception {
  final String message;
  final int statusCode;
  final String responseBody;

  SmsAuthException(this.message, this.statusCode, this.responseBody);

  @override
  String toString() => 'SmsAuthException($statusCode): $message';
}

> Key insight: The entire backend integration is ~60 lines of Dart. No SDK installation, no initialization ceremony, no platform-specific setup.


Step 3: Phone Number Input Screen

// lib/screens/phone_input_screen.dart
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
import 'otp_verify_screen.dart';

class PhoneInputScreen extends StatefulWidget {
  const PhoneInputScreen({super.key});

  @override
  State createState() => _PhoneInputScreenState();
}

class _PhoneInputScreenState extends State {
  final _phoneController = TextEditingController();
  final _authService = SmsAuthService('YOUR_API_KEY');
  bool _isLoading = false;

  Future _sendVerificationCode() async {
    final phone = _phoneController.text.trim();
    if (phone.isEmpty || phone.length < 10) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Please enter a valid phone number')),
      );
      return;
    }

    setState(() => _isLoading = true);

    try {
      await _authService.sendCode(phone);
      if (mounted) {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (_) => OtpVerifyScreen(
              phoneNumber: phone,
              authService: _authService,
            ),
          ),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to send code: $e')),
        );
      }
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Phone Verification')),
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text(
              'Enter your phone number',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            const Text('We\'ll send you a verification code via SMS'),
            const SizedBox(height: 24),
            TextField(
              controller: _phoneController,
              keyboardType: TextInputType.phone,
              decoration: const InputDecoration(
                hintText: '+1 (555) 123-4567',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.phone),
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _isLoading ? null : _sendVerificationCode,
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
              child: _isLoading
                  ? const SizedBox(
                      height: 20,
                      width: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Text('Send Verification Code'),
            ),
          ],
        ),
      ),
    );
  }
}

Step 4: OTP Verification Screen with Timer

Using the pin_code_fields package, we create a polished 6-digit OTP input with auto-submit, countdown timer, and resend functionality:

// lib/screens/otp_verify_screen.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pin_code_fields/pin_code_fields.dart';
import '../services/auth_service.dart';

class OtpVerifyScreen extends StatefulWidget {
  final String phoneNumber;
  final SmsAuthService authService;

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

  @override
  State createState() => _OtpVerifyScreenState();
}

class _OtpVerifyScreenState extends State {
  String _otpCode = '';
  bool _isLoading = false;
  int _remainingSeconds = 180; // 3-minute timer
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _startTimer();
  }

  void _startTimer() {
    _timer?.cancel();
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (_remainingSeconds > 0) {
        setState(() => _remainingSeconds--);
      } else {
        timer.cancel();
      }
    });
  }

  String get _timerText {
    final m = _remainingSeconds ~/ 60;
    final s = _remainingSeconds % 60;
    return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
  }

  Future _verifyCode() async {
    if (_otpCode.length != 6) return;
    setState(() => _isLoading = true);

    try {
      await widget.authService.verifyCode(
        phoneNumber: widget.phoneNumber,
        code: _otpCode,
      );

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Verification successful!'),
            backgroundColor: Colors.green,
          ),
        );
        Navigator.popUntil(context, (route) => route.isFirst);
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Verification failed: $e')),
        );
      }
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  Future _resendCode() async {
    await widget.authService.sendCode(widget.phoneNumber);
    setState(() => _remainingSeconds = 180);
    _startTimer();
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Enter Verification Code')),
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              'Code sent to ${widget.phoneNumber}',
              style: const TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              'Time remaining: $_timerText',
              style: TextStyle(
                color: _remainingSeconds < 30
                    ? Colors.red
                    : Colors.grey[600],
                fontSize: 14,
              ),
            ),
            const SizedBox(height: 32),
            PinCodeTextField(
              appContext: context,
              length: 6,
              onChanged: (value) => _otpCode = value,
              onCompleted: (_) => _verifyCode(),
              pinTheme: PinTheme(
                shape: PinCodeFieldShape.box,
                borderRadius: BorderRadius.circular(8),
                fieldHeight: 50,
                fieldWidth: 45,
                activeFillColor: Colors.white,
                activeColor: Colors.blue,
                selectedColor: Colors.blue,
                inactiveColor: Colors.grey.shade300,
              ),
              keyboardType: TextInputType.number,
              animationType: AnimationType.fade,
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _isLoading ? null : _verifyCode,
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
              child: _isLoading
                  ? const SizedBox(
                      height: 20,
                      width: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Text('Verify'),
            ),
            const SizedBox(height: 8),
            TextButton(
              onPressed: _remainingSeconds == 0 ? _resendCode : null,
              child: const Text('Resend Code'),
            ),
          ],
        ),
      ),
    );
  }
}

Complete Flow at a Glance

User → Enter phone number → POST /send → Receive SMS
     → Enter OTP code     → POST /verify → Verified ✓

That's the entire architecture. Two screens, two API calls, zero platform configuration.


Firebase Phone Auth vs REST API: A Comparison

| Aspect | Firebase Phone Auth | REST API Approach | |--------|-------------------|-------------------| | Initial Setup | Firebase project + SHA keys + Cloud Console | API key only | | Paperwork | None (but complex config) | None | | Time to Implement | 30-60 minutes | ~5 minutes | | Cost | 10 free/month, then $0.06/verification | ~$0.01-0.02/verification | | Customization | Limited | Full control | | Platform Dependencies | Firebase SDK required | HTTP requests only | | Bundle Size Impact | +2-5 MB | Negligible |


Production Tips & Security Best Practices

1. Never Hardcode API Keys

// ❌ Don't do this
const apiKey = 'sk-live-xxxxx';

// ✅ Use compile-time environment variables
const apiKey = String.fromEnvironment('SMS_API_KEY');
flutter run --dart-define=SMS_API_KEY=your-key-here

For production builds, consider a backend proxy that holds the API key server-side.

2. Phone Number Validation

/// Validate phone numbers before sending to API
bool isValidPhone(String phone) {
  // International format
  return RegExp(r'^\+?[1-9]\d{6,14}$').hasMatch(phone);
}

/// Korean phone number validation
bool isValidKoreanPhone(String phone) {
  return RegExp(r'^01[016789]\d{7,8}$').hasMatch(phone);
}

3. Client-Side Rate Limiting

Prevent users from spamming the send button:

DateTime? _lastSendTime;

bool get _canResend {
  if (_lastSendTime == null) return true;
  return DateTime.now().difference(_lastSendTime!) >
      const Duration(seconds: 60);
}

4. OTP Auto-Fill Support

For a better user experience, consider adding SMS auto-fill:

  • Android: Use the otp_autofill or sms_autofill package which leverages the SMS Retriever API
  • iOS: OTP auto-fill works automatically via the keyboard suggestion bar when using TextInputType.number

5. Robust Error Handling

try {
  await authService.sendCode(phone);
} on SocketException {
  showError('No internet connection');
} on TimeoutException {
  showError('Request timed out. Please try again.');
} on SmsAuthException catch (e) {
  if (e.statusCode == 429) {
    showError('Too many attempts. Please wait.');
  } else {
    showError('Something went wrong. Please try again.');
  }
}

Wrapping Up

Adding phone verification to your Flutter MVP doesn't have to be a multi-hour ordeal. With a simple REST API approach, you can have SMS verification running in under 5 minutes — no Firebase project setup, no SHA key registration, no platform-specific configuration files.

If you're looking for an SMS verification API that you can start using immediately without any paperwork or business registration documents, check out EasyAuth. You get 10 free verifications on signup to test your integration, and ongoing pricing starts at just $0.01-0.02 per verification — significantly cheaper than most alternatives. It's designed specifically for developers who want to ship fast and focus on what matters: building their product.

Stop wrestling with infrastructure. Start shipping features.


Tags: #Flutter #SMSVerification #OTP #MVP #MobileAuth #EasyAuth

비트베이크에서 광고를 시작해보세요

광고 문의하기

다른 글 보기

2026-04-08T11:02:47.515Z

2026 Professionals Solo Party & Wine Mixer Complete Guide: Real Reviews and Success Tips for Korean Singles

2026-04-08T11:02:47.487Z

2026년 직장인 솔로파티 & 와인모임 소개팅 완벽 가이드 - 실제 후기와 성공 팁

2026-04-08T10:03:28.247Z

Complete Google NotebookLM Guide 2026: Master the New Studio Features, Video Overviews, and Gemini Canvas Integration

2026-04-08T10:03:28.231Z

2026년 구글 NotebookLM 완벽 가이드: 새로운 스튜디오 기능, 비디오 개요 및 제미나이 캔버스 통합 실전 활용법

서비스

피드자주 묻는 질문고객센터

문의

비트베이크

레임스튜디오 | 사업자 등록번호 : 542-40-01042

경기도 남양주시 와부읍 수례로 116번길 16, 4층 402-제이270호

트위터인스타그램네이버 블로그