Files
tajniak81andClaude Opus 5.5 0fc8ee809d Mobile: QR-code verification
When both sides support it, a verification offers QR codes first: this
device shows its code (in a white quiet zone, whatever the theme) and can
scan the other's with the camera (mobile_scanner; only codes that start
with MATRIX count). The side whose code was scanned confirms the other
saw it succeed. Compare emoji stays available, and is used directly when
either side has no QR support.

Adds the CAMERA permission (camera hardware not required).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:16:27 +02:00

124 lines
3.5 KiB
Dart

import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
/// Scans the other side's verification QR code with the camera and returns
/// its bytes.
///
/// A verification code is binary, not text: it starts with "MATRIX", then
/// the version, the mode, the transaction and the keys. Codes that don't
/// start that way are ignored, so pointing the camera at something else
/// doesn't end the scan.
Future<Uint8List?> scanVerificationCode(BuildContext context) =>
Navigator.of(context).push<Uint8List>(
MaterialPageRoute(
fullscreenDialog: true,
builder: (_) => const _QrScanScreen(),
),
);
final _prefix = ascii.encode('MATRIX');
bool _isVerificationCode(Uint8List bytes) {
if (bytes.length < _prefix.length) return false;
for (var i = 0; i < _prefix.length; i++) {
if (bytes[i] != _prefix[i]) return false;
}
return true;
}
class _QrScanScreen extends StatefulWidget {
const _QrScanScreen();
@override
State<_QrScanScreen> createState() => _QrScanScreenState();
}
class _QrScanScreenState extends State<_QrScanScreen> {
final _controller = MobileScannerController(
formats: const [BarcodeFormat.qrCode],
);
bool _done = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onDetect(BarcodeCapture capture) {
if (_done) return;
for (final barcode in capture.barcodes) {
final bytes = switch (barcode.rawDecodedBytes) {
DecodedBarcodeBytes(:final bytes) => bytes,
DecodedVisionBarcodeBytes(:final bytes) => bytes,
_ => null,
};
if (bytes != null && _isVerificationCode(bytes)) {
_done = true;
Navigator.pop(context, bytes);
return;
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
shape: const Border(),
title: const Text(
'Scan their code',
style: TextStyle(color: Colors.white),
),
),
body: Stack(
children: [
MobileScanner(
controller: _controller,
onDetect: _onDetect,
errorBuilder: (context, error) => Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Text(
error.errorCode == MobileScannerErrorCode.permissionDenied
? 'Warp needs the camera to scan the code. Allow it in '
'the app settings, or compare emoji instead.'
: "The camera isn't available.",
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white),
),
),
),
),
Center(
child: Container(
width: 260,
height: 260,
decoration: BoxDecoration(
border: Border.all(color: Colors.white70, width: 3),
borderRadius: BorderRadius.circular(18),
),
),
),
const Positioned(
left: 24,
right: 24,
bottom: 48,
child: Text(
"Point the camera at the code on the other device.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 15),
),
),
],
),
);
}
}