Code With Bisky

How to implement authentication in your Flutter app using Appwrite

In this tutorial series, we will guide you through the process of integrating Appwrite authentication with Flutter, the popular cross-platform development framework. Whether you're a beginner or an experienced developer, this series is designed to help you understand the ins and outs of Appwrite authentication and how to implement it seamlessly in your Flutter applications.

Key Topics covered:

  • Appwrite setup and configuration
  • Phone Verification with Appwrite
  • Bloc Configurations

Description:

The tutorial will walk you through the process of phone number authentication with Appwrite. The OTP message will be sent and validated. We are going to use flutter_bloc dependency.

We are going to create AuthState, AuthEvent and AuthBloc classes.

  • create below classes
  • lib/bloc/auth/auth_state.dart
  • lib/bloc/auth/auth_event.dart
  • lib/bloc/auth/auth_bloc.dart

Code Snippet(AuthEvent):


 import 'package:equatable/equatable.dart';

abstract class  AuthEvent extends Equatable{


  @override
  List<Object> get props => [];
}


class MobileNumberLoginEvent extends AuthEvent{

  String mobileNumber;

  MobileNumberLoginEvent(this.mobileNumber);

  @override
  List<Object> get props => [mobileNumber];

}
class MobileNumberVerificationEvent extends AuthEvent{

  String userId;
  String secret;

  MobileNumberVerificationEvent(this.userId,this.secret);

  @override
  List<Object> get props => [userId,secret];

}
    

Code Snippet(AuthState):



import 'package:appwrite/models.dart';
import 'package:equatable/equatable.dart';

abstract class  AuthState extends Equatable{

  @override
  List<Object> get props => [];
}

class InitialAuthState extends AuthState{}
class LoadingLoginAuthState extends AuthState{}
class SuccessLoginAuthState extends AuthState{

  Token token;
  SuccessLoginAuthState(this.token);
  @override
  List<Object> get props => [token];

}

class FailureLoginAuthState extends AuthState{

  String message;
  FailureLoginAuthState(this.message);
  @override
  List<Object> get props => [message];
}

class LoadingOtpVerificationAuthState extends AuthState{}

class FailureOtpVerificationAuthState extends AuthState{

  String message;
  FailureOtpVerificationAuthState(this.message);
  @override
  List<Object> get props => [message];
}

class SuccessOtpVerificationAuthState extends AuthState{

  Session session;
  SuccessOtpVerificationAuthState(this.session);
  @override
  List<Object> get props => [session];
}
    

Code Snippet(AuthBloc):


import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:bloc/bloc.dart';
import 'package:chat_with_bisky/bloc/auth/auth_event.dart';
import 'package:chat_with_bisky/bloc/auth/auth_state.dart';
import 'package:chat_with_bisky/service/AppwriteClient.dart';
import 'package:kiwi/kiwi.dart';

class AuthBloc extends Bloc<AuthEvent,AuthState>{


  final AppWriteClientService  appWriteClientService= KiwiContainer().resolve<AppWriteClientService>();

  AuthBloc(AuthState authState) : super(authState){

    on<MobileNumberLoginEvent>((event,state) async{
      await loginWithMobileNumber(event,state);
    });

    on<MobileNumberVerificationEvent>((event,state) async{
      await mobileNumberVerification(event,state);
    });

  }


  loginWithMobileNumber(MobileNumberLoginEvent event, Emitter<AuthState> state) async {

    emit(LoadingLoginAuthState());
    try{

      Account account = Account(appWriteClientService.getClient());
      String mobileNumber = event.mobileNumber;
      Token token = await account.createPhoneSession(
        userId: mobileNumber.substring(1), //+32444444
        phone: mobileNumber,
      );


      print(token.$id);
      print(token.userId);
      emit(SuccessLoginAuthState(token));
    }catch (exception){
      print(exception);
      emit(FailureLoginAuthState("Error, please try again later"));
    }

  }

  mobileNumberVerification(MobileNumberVerificationEvent event, Emitter<AuthState> state) async {

    emit(LoadingOtpVerificationAuthState());
    try{

      Account account = Account(appWriteClientService.getClient());
      Session session = await account.updatePhoneSession(
        userId: event.userId,
        secret: event.secret,
      );
      emit(SuccessOtpVerificationAuthState(session));

    }catch (exception){
      print(exception);
      emit(FailureOtpVerificationAuthState("Invalid code supplied"));
    }
  }
}

    

We are using kiwi library for dependency injection. Let's create Appwrite client and configure it with kiwi

Code Snippet(AppwriteClient):

  • create below class
  • lib/service/AppwriteClient.dart

import 'package:appwrite/appwrite.dart';

class AppWriteClientService {

  Client getClient() {
    return Client()
        .setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
        .setProject('');// todo update with yours
  }
}

        
        

We created the Appwrite Client object that we are going to use for our Appwrite operations. Let's now add it into IOC

  • create directory dependency-injection
  • lib/dependency-injection/injection.dart

Code Snippet (Injection):


import 'package:chat_with_bisky/service/AppwriteClient.dart';
import 'package:kiwi/kiwi.dart';

void initializeKiwi(){

  KiwiContainer container=KiwiContainer();
  container
  .registerFactory((container) => AppWriteClientService());
}
        

We initialize it into our main method before runApp(). We can also initialize our Bloc so that we will be able to fire events and listens to the state through out the application.

Code Snippet (Bloc & Dependency Injection) initialization:


void main() {
 initializeKiwi();
  runApp(MultiBlocProvider(providers: [

    BlocProvider<AuthBloc>(
      create: (context) => AuthBloc((InitialAuthState())),
    )
  ],
      child: MyApp()));
}

    

Code Snippet (Login.dart) modification:

We can fire MobileNumberLoginEvent and listen to the Auth state. In our getPhoneNumber method let's refactor the code to be like this.


void getPhoneNumber(PhoneNumber number) async {
    String phone = number.phoneNumber!;
    BlocProvider.of<AuthBloc>(context)
    .add(MobileNumberLoginEvent(phone));

  }

        
        

Add equatable dependency in your pubspec.yaml


equatable: ^2.0.5
        
        

The body of Scaffold must be wrapped it with BlocListener<AuthBloc,AuthState>

Code Snippet (Login.dart) modification:



body: BlocListener<AuthBloc,AuthState>(

        listener: (context, state) {
          if (state is LoadingLoginAuthState){
            ChatDialogs.waiting(context, "loading.....");
          }else if (state is FailureLoginAuthState){
            ChatDialogs.hideProgress(context);
            ChatDialogs.informationOkDialog(context, title: "Error", description: state.message, type: AlertType.error);
          }else if (state is SuccessLoginAuthState){
            ChatDialogs.hideProgress(context);
            print("navigate to another page otp confirmation");
            AutoRouter.of(context).push(OtpPage(userId: state.token.userId));
          }
        },
        child:  Stack(
          children: [
            Image.asset(ImagePath.background, height: MediaQuery.of(context).size.height,),
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                InternationalPhoneNumberInput(
                  onInputChanged: (PhoneNumber number) {
                    this.number = number;
                  },
                  onInputValidated: (bool value) {
                  },
                  selectorConfig: const SelectorConfig(
                    selectorType: PhoneInputSelectorType.BOTTOM_SHEET,
                  ),
                  ignoreBlank: false,
                  autoValidateMode: AutovalidateMode.disabled,
                  selectorTextStyle: TextStyle(color: Colors.black),
                  initialValue: number,
                  textFieldController: controller,
                  formatInput: true,
                  keyboardType:
                  const TextInputType.numberWithOptions(signed: true, decimal: true),
                  inputBorder: const OutlineInputBorder(),
                  onSaved: (PhoneNumber number) {
                    print('On Saved: $number');
                  },
                ),
                ElevatedButton(
                  onPressed: () {
                    formKey.currentState?.validate();
                    getPhoneNumber(number);
                  },
                  child: const Text('Verify'),
                ),
              ],)
          ],
        ),
      )
        
        

Code Snippet (OtpPage.dart):

Create a lib/pages/otp/OtpPage.dart file and add below code to validate the phone number.


import 'package:auto_route/annotations.dart';
import 'package:chat_with_bisky/bloc/auth/auth_bloc.dart';
import 'package:chat_with_bisky/bloc/auth/auth_event.dart';
import 'package:chat_with_bisky/values/values.dart';
import 'package:chat_with_bisky/widget/custom_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl_phone_number_input/intl_phone_number_input.dart';
import 'package:introduction_screen/introduction_screen.dart';
import 'package:otp_text_field/otp_text_field.dart';
import 'package:otp_text_field/style.dart';
import 'package:rflutter_alert/rflutter_alert.dart';
import '../../bloc/auth/auth_state.dart';

@RoutePage()
class OtpPage extends StatefulWidget{
  String userId;
  OtpPage(this.userId);

  @override
  _OtpPageState  createState() {
    return _OtpPageState();
  }

}
class  _OtpPageState extends State<OtpPage>{
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();
  final TextEditingController controller = TextEditingController();

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body: BlocListener<AuthBloc,AuthState>(
        listener: (context, state) {
          if (state is LoadingOtpVerificationAuthState){
            ChatDialogs.waiting(context, "loading.....");
          }else if (state is FailureOtpVerificationAuthState){
            ChatDialogs.hideProgress(context);
            ChatDialogs.informationOkDialog(context, title: "Error", description: state.message, type: AlertType.error);
          }else if (state is SuccessOtpVerificationAuthState){
            ChatDialogs.hideProgress(context);
            // persist user id and also the stage
            print("navigate to another page username page");
          }
        },
        child:  Stack(
          children: [
            Image.asset(ImagePath.background, height: MediaQuery.of(context).size.height,
            ),
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                OTPTextField(
                  otpFieldStyle: OtpFieldStyle(
                      backgroundColor: Colors.white, focusBorderColor: Colors.amber),
                  length: 6,
                  width: MediaQuery.of(context).size.width,
                  // fieldWidth: 40,
                  style: const TextStyle(fontSize: 17),
                  textFieldAlignment: MainAxisAlignment.spaceAround,
                  fieldStyle: FieldStyle.box,
                  onCompleted: (String code) {
                    controller.text = code;
                    BlocProvider.of<AuthBloc>(context)
                    .add(MobileNumberVerificationEvent(widget.userId,code));
                  },
                  onChanged: (String changed) {},
                  obscureText: false,
                ),
              ],)
          ],
        ),
      )
    );
  }
  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

        
        

Don't forget to run the following command in your terminal to generate OTPPage route flutter packages pub run build_runner build. Let's add the route in lib/route/app_route/AppRouter.dart


AutoRoute(page: OtpPage.page),
        
        

Conclusion:

We managed to implement Appwrite Authentication in AuthBloc and fire Auth events and listens to the Auth states. The user is now able to login and validate the OTP. We also configured the dependency injection and initialize it into main method. In the next Tutorial we will implement profile picture update and updating the name of the user. Don't forget to share and join our Discord Channel, May you please subscribe to our YouTube Channel