Code With Bisky

Mastering Secure Login in Flutter with Appwrite and Riverpod: Building the Ultimate Chat App

Welcome to our tutorial on building a Bisky Chat application login feature using Flutter, Appwrite, and Riverpod! In this step-by-step guide, we'll show you how to create a secure and user-friendly login system for your chat app without relying on Firebase.

Key Topics covered:

  • ARefactor Login page
  • Configure Riverpod

Description:

The tutorial will walk you through the process of phone number authentication with Appwrite. We are using Riverpod instead of flutter_bloc.

Other Dependencies That we are supposed to add are:

Add the following dependencies in your pubspec.yaml


dependencies:
  riverpod_annotation: ^2.1.1
  freezed: ^2.3.4
  freezed_annotation: ^2.2.0
  hooks_riverpod: ^2.3.6
  flutter_hooks: ^0.18.6

            //add below dependency under dev_dependencies
dev_dependencies:
  riverpod_generator: ^2.2.3
        
        
  • Add the following code below in lib/constant/strings.dart. You can update with your own id's from Appwrite

static const projectId = "6478980109c06f36e4b1";
        
        

Code Snippet(AuthenticationState):

Create an AuthenticationState.dart and add annotation @freezed

  • lib/model/AuthenticationState.dart

 import 'package:freezed_annotation/freezed_annotation.dart';

part 'AuthenticationState.freezed.dart';

@freezed
class AuthenticationState with _$AuthenticationState {
  factory AuthenticationState({
    @Default('') String phoneNumber,
    @Default('') String secret,
    @Default(false) bool loading,
  }) = _AuthenticationState;
}
    

Don't forget to run the following command in your terminal to generate AuthenticationState.freezed.dart flutter packages pub run build_runner build

Code Snippet(main.dart) modification:

Configure Riverpod in main.dart. Wrap MultiBlocProvider with ProviderScope() that's all

  • lib/main.dart
            
 runApp(ProviderScope(
    child: MultiBlocProvider(providers: [

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

Code Snippet(AppwriteClientProvider):

Create an AppwriteClientProvider.dart. We are using riverpod Provider to manage this class AppwriteClientProvider. The provider will be available throughout the application

  • lib/core/providers/AppwriteClientProvider.dart

import 'package:appwrite/appwrite.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final appwriteClientProvider = Provider<Client>(
      (ref)  {
    final channel = Client(
      endPoint: "https://cloud.appwrite.io/v1",
    );
    channel.setProject(Strings.projectId);
    channel.setSelfSigned();
    return channel;
  },
);
        
        

Code Snippet(AppwriteAccountProvider):

Create an AppwriteAccountProvider.dart. We are using riverpod Provider to manage it

  • lib/core/providers/AppwriteAccountProvider.dart

import 'package:appwrite/appwrite.dart';
import 'package:chat_with_bisky/core/providers/AppwriteClientProvider.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final appwriteAccountProvider=  Provider((ref) => Account(ref.read(appwriteClientProvider)));

        
        

Code Snippet(AuthViewModel):

Create an AuthViewModel.dart. We will add our login functionality into this class. Will annotate it with @riverpod

  • lib/pages/login/providers/AuthViewModel.dart

import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:chat_with_bisky/core/providers/AppwriteAccountProvider.dart';
import 'package:chat_with_bisky/model/AuthenticationState.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'AuthViewModel.g.dart';

@riverpod
class AuthNotifier extends _$AuthNotifier{

  Account get _account=> ref.read(appwriteAccountProvider);

  @override
  AuthenticationState build(){

    return AuthenticationState();
  }

  void phoneNumberChanged(String string){

    state = state.copyWith(phoneNumber: string);
  }

  Future<void> loginWithMobileNumber() async {

    try{
      state = state.copyWith(loading: true);
      String mobileNumber = state.phoneNumber;
      Token token = await _account.createPhoneSession(
        userId: mobileNumber.substring(1), //+32444444
        phone: mobileNumber,
      );

      state = state.copyWith(phoneNumber: mobileNumber.substring(1));
      state = state.copyWith(loading: false);
    }on AppwriteException catch  (exception){

      print(exception);

      state = state.copyWith(loading: false);

      return Future.error(exception.message ?? '${exception.code}');
    }
  }
}

    

Don't forget to run the following command in your terminal to generate AuthViewModel.g.dart flutter packages pub run build_runner build

Code Snippet(Login.dart) modification:

We are extending ConsumerWidget from riverpod to have access to ref in build method. We are using the watch from riverpod to listen to the state of the Authentication. The ref.read(authNotifierProvider.notifier) is to get the authNotifierProvider notifier. We have access to the view model and call the login method.


import 'package:auto_route/annotations.dart';
import 'package:auto_route/auto_route.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/model/AuthenticationState.dart';
import 'package:chat_with_bisky/pages/login/providers/AuthViewModel.dart';
import 'package:chat_with_bisky/service/LocalStorageService.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:intl_phone_number_input/intl_phone_number_input.dart';
import 'package:introduction_screen/introduction_screen.dart';
import 'package:rflutter_alert/rflutter_alert.dart';

import '../../bloc/auth/auth_state.dart';
import '../../route/app_route/AppRouter.gr.dart';

@RoutePage()
class LoginPage extends ConsumerWidget {
  final GlobalKey<FormState> formKey = GlobalKey<FormState>();

  final TextEditingController controller = TextEditingController();
  String initialCountry = 'BE';
  PhoneNumber number = PhoneNumber(isoCode: 'BE');

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final theme = Theme.of(context);
    final scheme = theme.colorScheme;
    final notifier = ref.read(authNotifierProvider.notifier);
    final model = ref.watch(authNotifierProvider);
    return Scaffold(
      body: 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, notifier, context, model);
                },
                child: const Text('Verify'),
              ),
            ],
          ),
          if (model.loading)
            Material(
              color: scheme.surfaceVariant.withOpacity(0.5),
              child: const Center(
                child: CircularProgressIndicator(),
              ),
            )
        ],
      ),
    );
  }

  void getPhoneNumber(PhoneNumber number, AuthNotifier notifier,
      BuildContext context, AuthenticationState model) async {
    String phone = number.phoneNumber!;
    LocalStorageService.putString(
        LocalStorageService.dialCode, number.dialCode ?? "");
    notifier.phoneNumberChanged(phone);
    await notifier.loginWithMobileNumber();

    print("navigate to another page otp confirmation");
    AutoRouter.of(context).push(OtpPage(userId: phone.substring(1)));
  }

  @override
  void dispose() {
    controller.dispose();
  }
}

    

Conclusion:

We managed to implement Appwrite Authentication with Riverpod and call the login method in Login Page. The user is now able to login and navigated to the OTP page. We also configured the riverpod and added necessary dependency required for you to get started. Don't forget to share and join our Discord Channel, May you please subscribe to our YouTube Channel