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>
124 lines
3.5 KiB
Dart
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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|