QR Auto Login com Flutter

Tempo de leitura: 5 minutes

Você já se perguntou como fazer login em outros dispositivos sem a necessidade de e-mails, senhas, PIN e muito mais? Aplicativos como Discord, WhatsApp e mais usam códigos QR para fazer login de outros dispositivos conectados e você também pode fazer isso com o Flutter

 

Primeira etapa: ativar o Firebase

Sim, a maneira mais fácil de fazer isso é criar um projeto com Firebase e configurá-lo para Web e Mobile (ou as plataformas que você deseja suportar e são suportadas pelo Firebase usando Flutter). Certifique-se de verificar o suporte atual para “Firebase Messaging”.

 

Plataformas de configuração

Cada plataforma suportada pelo Flutter precisa de um conjunto específico de parâmetros e classes que você precisa criar para habilitar o Firebase Messaging, incluindo Web… que tem muitas pequenas configurações como incluir este arquivo firebase-messaging-sw.js que você pode perder ao ler o >>> Docs <<<.

Depois de seguir todas as etapas e instalar a CLI do Firebase (flutterfire), uma nova classe chamada: “firebase_options.dart” será gerada automaticamente e estará pronta para uso. Isso permitirá que você inicie rapidamente o Firebase dentro do seu aplicativo Flutter:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform
  );

  runApp(const MainApp());
}

 

Funções do Firebase

Esta etapa pode parecer um pouco complicada e difícil se você não desenvolveu com Js, mas é super direta e ajudará você a realizar o truque.

Por que estamos usando esse método? Bem… precisamos de segurança porque uma maneira conveniente de fazer login não pode comprometer uma parte tão importante do nosso aplicativo, usuários, confiança e muito mais.

Para atingir nosso objetivo, obteremos o ID do usuário e criaremos um token personalizado para registrar nossos usuários:

const functions = require("firebase-functions");
const admin = require('firebase-admin');

const serviceAccount = require("./qr-to-login-firebase-adminsdk.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

exports.getToken = functions.https.onCall(async (data, context) => {
  return admin
  .auth()
  .createCustomToken(data.uid)
  .then((customToken) => {
      return customToken;
    }).catch((error) => {
      return null;
    });
});

Assim que enviarmos esta Firebase Function para o seu projeto, ela estará “escutando” suas chamadas do lado do cliente:

final reqToken = await function.httpsCallable('getToken').call({'uid': user.uid});
if (reqToken.data != null) {
  final customToken = reqToken.data as String;
  // Do your logic to send the token
}

como podem ver, só precisamos do “UID” do usuário logado e solicitar um Token para ele.

Assim que tivermos esses dados, é hora de ENVIAR O PUSH (Notificação)!

 

Notificações via push

Existem algumas maneiras de fazer isso, mas, para este exemplo, usaremos uma lógica do lado do cliente para enviar o push:

class PushNotifications {

  Future<bool> send({
    String title = 'QrToLogin',
    String? deviceToken,
    String? token,
  }) async {
    const String url = 'https://fcm.googleapis.com/fcm/send';

    final Map<String, dynamic> data = {
      'notification': {
        'title': title,
      },
      'priority': 'high',
      'data': {
        'status': 'done',
        'click_action': 'FLUTTER_NOTIFICATION_CLICK',
        'token': token
      },
      'to': '$deviceToken',
      'message': {
        'token': '$deviceToken'
      }
    };
    final Map<String, String> headers = {
      'Content-Type': 'application/json',
      'Authorization': Constants.authKey,
    };

    final result = await http.post(
      Uri.parse(url),
      body: jsonEncode(data),
      encoding: Encoding.getByName('utf-8'),
      headers: headers,
    );

    return result.statusCode == 200;
  }

  Future<bool> sendToTopic(String id, {
    String topic = 'all',
    String title = 'QrToLogin',
    String token = '',
  }) async {
    const String url = 'https://fcm.googleapis.com/fcm/send';

    final Map<String, dynamic> data = {
      'notification': {
        'title': title,
      },
      'priority': 'high',
      'data': {
        'id': id,
        'status': 'done',
        'click_action': 'FLUTTER_NOTIFICATION_CLICK',
        'token': token,
      },
      'topic': topic,
    };

    final Map<String, String> headers = {
      'Content-Type': 'application/json',
      'Authorization': Constants.authKey,
    };

    final result = await http.post(
      Uri.parse(url),
      body: jsonEncode(data),
      encoding: Encoding.getByName('utf-8'),
      headers: headers,
    );

    return result.statusCode == 200;
  }
}

Você notará uma variável “Constants.authKey” e, sim, precisará obter a sua no Firebase Console como chave do servidor:

Lembre-se de que esta implementação é “Legado” e pode não funcionar no futuro. Certifique-se de seguir a implementação mais recente do Push Notification ao ler esta postagem.

Assim que obtivermos o token anteriormente, agora precisamos enviá-lo:

/.../ 
final customToken = reqToken.data as String;

final push = PushNotifications();
push.send(deviceToken: token, token: customToken);

Agora, o que é “deviceToken” você pode perguntar… bem, é o QR Code do outro dispositivo do Cliente. O QR Code será gerado pelos dados do Token que iremos ler do nosso dispositivo logado e enviar o Push Notification para o dispositivo que está aguardando para ser logado. Algo assim:

Dispositivo aguardando -> Gera código QR -> Aguarda a notificação por push

Dispositivo registrado -> Lê código QR -> Gera token -> Enviar notificação por push com token personalizado

Parece estranho, mas uma vez implementado, toda a mágica ganha vida!

Estaremos usando o plugin “qr_flutter” para mostrar nosso QR Code:

QrImage(
    data: _qr,
    version: QrVersions.auto,
    embeddedImage: const AssetImage(
      'assets/flutter_log.png', // optional
    ),
    embeddedImageStyle: QrEmbeddedImageStyle(
      size: const Size(55, 55),
    ),
    size: 250,
  )

e estaremos obtendo dados para preenchê-lo + aguardando a notificação push assim:

Future<String?> getToken() async {
  String token = '';

  final messaging = FirebaseMessaging.instance;

  NotificationSettings settings = await messaging.requestPermission(
    alert: true,
    announcement: false,
    badge: true,
    carPlay: false,
    criticalAlert: false,
    provisional: false,
    sound: true,
  );

  if (settings.authorizationStatus == AuthorizationStatus.authorized) {
    token = await FirebaseMessaging.instance.getToken();
  }
    return token;
}

em seguida, os toques finais e estamos prontos para ir:

Future<void> _generateQr() async {
  final push = WebPush();
  final token = await push.getToken();
  setState(() => _qr = token ?? '');

  FirebaseMessaging.onMessage.listen((event) async {
    _load(); // change any UI loading state
    final auth = FirebaseAuth.instance;
    final token = event.data['token'];
    final login = await auth.signInWithCustomToken(token); // MAGIC!
    if (login.user != null) {
     Navigator.of(context).push(
      MaterialPageRoute(builder: (_) => const HomeScreen()), // Do anything
     );
    }
    _load(); // change any UI loading state
  });
}

 

signInWithCustomToken

Este método faz toda a “mágica” acontecer. Isso vem da Firebase Function que fizemos e foi solicitada pelo dispositivo logado. Assim que tivermos o token do dispositivo para direcionar para onde enviar a notificação por push e o token personalizado para fazer login sem credenciais (manualmente), todo o processo parece super simples!

QR Scan

Como parte final desta demonstração, darei a você o trecho para QR Scan Codes:

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';

class QrScanScreen extends StatefulWidget {
  const QrScanScreen({Key? key}) : super(key: key);

  @override
  _QrScanScreenState createState() => _QrScanScreenState();
}

class _QrScanScreenState extends State<QrScanScreen> {
  final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
  Barcode? result;
  QRViewController? controller;

  @override
  void reassemble() {
    super.reassemble();
    if (Platform.isAndroid) {
      controller!.pauseCamera();
    } else if (Platform.isIOS) {
      controller!.resumeCamera();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: QRView(
        key: qrKey,
        onQRViewCreated: _onQRViewCreated,
      ),
    );
  }

  void _onQRViewCreated(QRViewController controller) {
    this.controller = controller;
    controller.scannedDataStream.listen((scanData) {
      this.controller?.stopCamera();
      Navigator.of(context).pop(scanData.code);
    });
  }

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

Depois de digitalizar um QR Code, esta tela será descartada e podemos enviar o Push que sabemos tanto sobre isso:

Future<void> _scanQrCode() async {
  final token = await Navigator.of(context).push<String?>(
    MaterialPageRoute(builder: (_) => const QrScanScreen()),
  );

  if (token != null) {
    _sendPush(token);
  }
}