Skip to main content
The Dynamic Flutter SDK provides email and SMS authentication through OTP (One-Time Password) verification. Users can sign in by receiving a verification code sent to their email address or phone number.
Email and SMS authentication must be enabled in your environment’s dashboard settings before they can be used in your application.

Email Authentication

Send Email OTP

Send a verification code to the user’s email address:
import 'package:dynamic_sdk/dynamic_sdk.dart';

// Send OTP to email
await DynamicSDK.instance.auth.email.sendOTP("[email protected]");

Verify Email OTP

Verify the OTP code entered by the user:
// Verify the code
await DynamicSDK.instance.auth.email.verifyOTP("123456");

Resend Email OTP

Allow users to request a new OTP code if the previous one expired or wasn’t received:
// Resend the code
await DynamicSDK.instance.auth.email.resendOTP();

Rate Limits

Email verification is subject to the following rate limits:
  • 3 attempts per 10 minutes per email address
This is in place to protect deliverability of emails and to prevent abuse.

SMS Authentication

Send SMS OTP

Send a verification code to the user’s phone number using the PhoneData structure:
import 'package:dynamic_sdk/dynamic_sdk.dart';

// Send OTP via SMS
await DynamicSDK.instance.auth.sms.sendOTP(
  PhoneData(
    dialCode: "+1",
    iso2: "US",
    phone: "5551234567"
  )
);

Verify SMS OTP

Verify the OTP code entered by the user:
// Verify the code
await DynamicSDK.instance.auth.sms.verifyOTP("123456");

Resend SMS OTP

Allow users to request a new OTP code:
// Resend the code
await DynamicSDK.instance.auth.sms.resendOTP();

PhoneData Structure

The SMS authentication methods use a PhoneData object that contains:
  • dialCode: The country dial code (e.g., “+1” for US, “+44” for UK)
  • iso2: The two-letter country code (e.g., “US”, “GB”, “JP”)
  • phone: The phone number without the country code
Examples:
// US phone number
PhoneData(dialCode: "+1", iso2: "US", phone: "5551234567")

// UK phone number
PhoneData(dialCode: "+44", iso2: "GB", phone: "7700900000")

// Japan phone number
PhoneData(dialCode: "+81", iso2: "JP", phone: "9012345678")

External JWT Authentication

For apps with existing authentication systems, you can authenticate users with an external JWT:
import 'package:dynamic_sdk/dynamic_sdk.dart';

await DynamicSDK.instance.auth.externalAuth.signInWithExternalJwt(
  props: SignInWithExternalJwtParams(jwt: yourJwtToken)
);

Authentication State

Check Current User

import 'package:dynamic_sdk/dynamic_sdk.dart';

// Get current authentication token
final token = DynamicSDK.instance.auth.token;

if (token != null) {
  print('User is authenticated');
}

Listen for Authentication Changes

Use streams to react to authentication state changes:
import 'package:dynamic_sdk/dynamic_sdk.dart';

// Listen for token changes
StreamBuilder<String?>(
  stream: DynamicSDK.instance.auth.tokenChanges,
  builder: (context, snapshot) {
    final token = snapshot.data;

    if (token != null) {
      return AuthenticatedView();
    } else {
      return LoginView();
    }
  },
)

// Listen for user changes
StreamBuilder<dynamic>(
  stream: DynamicSDK.instance.auth.authenticatedUserChanges,
  builder: (context, snapshot) {
    final user = snapshot.data;

    if (user != null) {
      return Text('Welcome, ${user.email}!');
    } else {
      return const Text('Please sign in');
    }
  },
)

Logout

import 'package:dynamic_sdk/dynamic_sdk.dart';

// Logout the current user
await DynamicSDK.instance.auth.logout();

Built-in UI

The easiest way to add authentication is using the built-in UI which handles all authentication methods:
import 'package:dynamic_sdk/dynamic_sdk.dart';

DynamicSDK.instance.ui.showAuth();

Complete Authentication Flow

Here’s a complete example combining email and SMS authentication:
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:flutter/material.dart';

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

  @override
  State<AuthenticationScreen> createState() => _AuthenticationScreenState();
}

class _AuthenticationScreenState extends State<AuthenticationScreen> {
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _phoneController = TextEditingController();
  final TextEditingController _otpController = TextEditingController();

  bool _otpSent = false;
  bool _useEmail = true; // Toggle between email and SMS

  Future<void> _sendOTP() async {
    try {
      if (_useEmail) {
        await DynamicSDK.instance.auth.email.sendOTP(_emailController.text);
      } else {
        await DynamicSDK.instance.auth.sms.sendOTP(
          PhoneData(
            dialCode: "+1",
            iso2: "US",
            phone: _phoneController.text,
          ),
        );
      }

      setState(() {
        _otpSent = true;
      });
    } catch (e) {
      // Handle error
      print('Error sending OTP: $e');
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Error sending OTP: $e')),
      );
    }
  }

  Future<void> _verifyOTP() async {
    try {
      if (_useEmail) {
        await DynamicSDK.instance.auth.email.verifyOTP(_otpController.text);
      } else {
        await DynamicSDK.instance.auth.sms.verifyOTP(_otpController.text);
      }

      // User is now authenticated - navigation handled by stream listeners
    } catch (e) {
      // Handle error
      print('Error verifying OTP: $e');
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Error verifying OTP: $e')),
      );
    }
  }

  Future<void> _resendOTP() async {
    try {
      if (_useEmail) {
        await DynamicSDK.instance.auth.email.resendOTP();
      } else {
        await DynamicSDK.instance.auth.sms.resendOTP();
      }

      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('OTP resent successfully')),
      );
    } catch (e) {
      // Handle error
      print('Error resending OTP: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_useEmail ? 'Email Authentication' : 'SMS Authentication'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // Toggle between email and SMS
            SegmentedButton<bool>(
              segments: const [
                ButtonSegment<bool>(value: true, label: Text('Email')),
                ButtonSegment<bool>(value: false, label: Text('SMS')),
              ],
              selected: {_useEmail},
              onSelectionChanged: (Set<bool> newSelection) {
                setState(() {
                  _useEmail = newSelection.first;
                  _otpSent = false;
                });
              },
            ),
            const SizedBox(height: 24),

            if (!_otpSent) ...[
              // Input for email or phone
              if (_useEmail)
                TextField(
                  controller: _emailController,
                  decoration: const InputDecoration(
                    labelText: 'Email Address',
                    border: OutlineInputBorder(),
                  ),
                  keyboardType: TextInputType.emailAddress,
                )
              else
                TextField(
                  controller: _phoneController,
                  decoration: const InputDecoration(
                    labelText: 'Phone Number',
                    border: OutlineInputBorder(),
                    helperText: 'Enter number without country code',
                  ),
                  keyboardType: TextInputType.phone,
                ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: _sendOTP,
                child: const Text('Send Verification Code'),
              ),
            ] else ...[
              // OTP verification
              Text(
                _useEmail
                    ? 'Enter the code sent to ${_emailController.text}'
                    : 'Enter the code sent to +1${_phoneController.text}',
              ),
              const SizedBox(height: 16),
              TextField(
                controller: _otpController,
                decoration: const InputDecoration(
                  labelText: 'Verification Code',
                  border: OutlineInputBorder(),
                ),
                keyboardType: TextInputType.number,
                maxLength: 6,
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: _verifyOTP,
                child: const Text('Verify Code'),
              ),
              const SizedBox(height: 8),
              TextButton(
                onPressed: _resendOTP,
                child: const Text('Resend Code'),
              ),
            ],
          ],
        ),
      ),
    );
  }

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

Configuration

Dashboard Settings

Configure email and SMS authentication in your Dynamic dashboard:
  1. Enable Email Authentication:
    • Go to Login & User Profile
    • Toggle “Email” on to enable email authentication
    • No additional configuration is required
  2. Enable SMS Authentication:
    • Go to Login & User Profile
    • Toggle “SMS” on to enable SMS authentication
    • You can optionally provide your own SMS provider credentials in the phone number settings
    • This is required for phone numbers outside of US and Canada

Best Practices

1. Error Handling

Always handle errors gracefully and provide clear feedback to users:
try {
  await DynamicSDK.instance.auth.email.sendOTP(email);
} catch (e) {
  if (e.toString().contains('rate limit')) {
    showError('Too many attempts. Please try again later.');
  } else if (e.toString().contains('invalid email')) {
    showError('Please enter a valid email address.');
  } else {
    showError('Failed to send OTP: $e');
  }
}

2. User Experience

  • Show loading states during authentication
  • Provide clear error messages
  • Allow users to resend OTP if needed
  • Display the email/phone where OTP was sent
  • Add countdown timer for resend button

3. Security

  • Never store OTP codes
  • Always use HTTPS connections
  • Implement proper session management
  • Handle token refresh automatically

Troubleshooting

OTP Not Received

  • Check that the email/phone number is valid
  • Look in spam folder for email OTP
  • Verify that the provider is enabled in Dynamic dashboard
  • Check rate limits haven’t been exceeded

Authentication Fails

  • Ensure OTP code is entered correctly
  • Check that the code hasn’t expired (codes typically expire after 10 minutes)
  • Verify network connectivity
  • Check for any error messages in logs

Session Not Persisting

  • Ensure you’re listening to tokenChanges or authenticatedUserChanges streams
  • Verify that the SDK is properly initialized
  • Check that you’re not clearing app data
  • Make sure the DynamicSDK.instance.dynamicWidget is included in your widget tree

What’s Next

Now that you have authentication set up:
  1. Social Authentication - Add social login options
  2. Session Management - Manage authenticated sessions with Streams
  3. Wallet Creation - Learn about automatic wallet creation
  4. Wallet Operations - Work with user wallets

Reference

For more details on the authentication API, see: