周睿 пре 1 година
родитељ
комит
816a508066

+ 50 - 0
app_business/lib/generated/base.pb.dart

@@ -1712,6 +1712,56 @@ class CourseInfo extends $pb.GeneratedMessage {
   $core.List<ControlPoint> get controlPointSortedList => $_getList(4);
 }
 
+class QrCodeInfo extends $pb.GeneratedMessage {
+  factory QrCodeInfo({
+    $core.String? qrCode,
+  }) {
+    final $result = create();
+    if (qrCode != null) {
+      $result.qrCode = qrCode;
+    }
+    return $result;
+  }
+  QrCodeInfo._() : super();
+  factory QrCodeInfo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory QrCodeInfo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'QrCodeInfo', package: const $pb.PackageName(_omitMessageNames ? '' : 'base.v1'), createEmptyInstance: create)
+    ..aOS(1, _omitFieldNames ? '' : 'qrCode', protoName: 'qrCode')
+    ..hasRequiredFields = false
+  ;
+
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  QrCodeInfo clone() => QrCodeInfo()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  QrCodeInfo copyWith(void Function(QrCodeInfo) updates) => super.copyWith((message) => updates(message as QrCodeInfo)) as QrCodeInfo;
+
+  $pb.BuilderInfo get info_ => _i;
+
+  @$core.pragma('dart2js:noInline')
+  static QrCodeInfo create() => QrCodeInfo._();
+  QrCodeInfo createEmptyInstance() => create();
+  static $pb.PbList<QrCodeInfo> createRepeated() => $pb.PbList<QrCodeInfo>();
+  @$core.pragma('dart2js:noInline')
+  static QrCodeInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<QrCodeInfo>(create);
+  static QrCodeInfo? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get qrCode => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set qrCode($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasQrCode() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearQrCode() => clearField(1);
+}
+
 class ControlPoint extends $pb.GeneratedMessage {
   factory ControlPoint({
     $fixnum.Int64? id,

+ 12 - 0
app_business/lib/generated/base.pbjson.dart

@@ -603,6 +603,18 @@ final $typed_data.Uint8List courseInfoDescriptor = $convert.base64Decode(
     'YXhSYW5nZRJQChljb250cm9sX3BvaW50X3NvcnRlZF9saXN0GAUgAygLMhUuYmFzZS52MS5Db2'
     '50cm9sUG9pbnRSFmNvbnRyb2xQb2ludFNvcnRlZExpc3Q=');
 
+@$core.Deprecated('Use qrCodeInfoDescriptor instead')
+const QrCodeInfo$json = {
+  '1': 'QrCodeInfo',
+  '2': [
+    {'1': 'qrCode', '3': 1, '4': 1, '5': 9, '10': 'qrCode'},
+  ],
+};
+
+/// Descriptor for `QrCodeInfo`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List qrCodeInfoDescriptor = $convert.base64Decode(
+    'CgpRckNvZGVJbmZvEhYKBnFyQ29kZRgBIAEoCVIGcXJDb2Rl');
+
 @$core.Deprecated('Use controlPointDescriptor instead')
 const ControlPoint$json = {
   '1': 'ControlPoint',

+ 40 - 0
app_business/lib/generated/track_offical.pbgrpc.dart

@@ -38,6 +38,14 @@ class ApiToClient extends $grpc.Client {
       '/to.v1.ApiTo/ToSignInByAppToken',
       ($1.ToSignInByAppTokenRequest value) => value.writeToBuffer(),
       ($core.List<$core.int> value) => $0.SignInReply.fromBuffer(value));
+  static final _$toGetSignInQrCode = $grpc.ClientMethod<$0.DefaultRequest, $0.QrCodeInfo>(
+      '/to.v1.ApiTo/ToGetSignInQrCode',
+      ($0.DefaultRequest value) => value.writeToBuffer(),
+      ($core.List<$core.int> value) => $0.QrCodeInfo.fromBuffer(value));
+  static final _$toQrCodeSignIn = $grpc.ClientMethod<$0.QrCodeInfo, $0.SignInReply>(
+      '/to.v1.ApiTo/ToQrCodeSignIn',
+      ($0.QrCodeInfo value) => value.writeToBuffer(),
+      ($core.List<$core.int> value) => $0.SignInReply.fromBuffer(value));
   static final _$toSignOutV2 = $grpc.ClientMethod<$0.DefaultRequest, $0.DefaultReply>(
       '/to.v1.ApiTo/ToSignOutV2',
       ($0.DefaultRequest value) => value.writeToBuffer(),
@@ -185,6 +193,14 @@ class ApiToClient extends $grpc.Client {
     return $createUnaryCall(_$toSignInByAppToken, request, options: options);
   }
 
+  $grpc.ResponseFuture<$0.QrCodeInfo> toGetSignInQrCode($0.DefaultRequest request, {$grpc.CallOptions? options}) {
+    return $createUnaryCall(_$toGetSignInQrCode, request, options: options);
+  }
+
+  $grpc.ResponseFuture<$0.SignInReply> toQrCodeSignIn($0.QrCodeInfo request, {$grpc.CallOptions? options}) {
+    return $createUnaryCall(_$toQrCodeSignIn, request, options: options);
+  }
+
   $grpc.ResponseFuture<$0.DefaultReply> toSignOutV2($0.DefaultRequest request, {$grpc.CallOptions? options}) {
     return $createUnaryCall(_$toSignOutV2, request, options: options);
   }
@@ -343,6 +359,20 @@ abstract class ApiToServiceBase extends $grpc.Service {
         false,
         ($core.List<$core.int> value) => $1.ToSignInByAppTokenRequest.fromBuffer(value),
         ($0.SignInReply value) => value.writeToBuffer()));
+    $addMethod($grpc.ServiceMethod<$0.DefaultRequest, $0.QrCodeInfo>(
+        'ToGetSignInQrCode',
+        toGetSignInQrCode_Pre,
+        false,
+        false,
+        ($core.List<$core.int> value) => $0.DefaultRequest.fromBuffer(value),
+        ($0.QrCodeInfo value) => value.writeToBuffer()));
+    $addMethod($grpc.ServiceMethod<$0.QrCodeInfo, $0.SignInReply>(
+        'ToQrCodeSignIn',
+        toQrCodeSignIn_Pre,
+        false,
+        false,
+        ($core.List<$core.int> value) => $0.QrCodeInfo.fromBuffer(value),
+        ($0.SignInReply value) => value.writeToBuffer()));
     $addMethod($grpc.ServiceMethod<$0.DefaultRequest, $0.DefaultReply>(
         'ToSignOutV2',
         toSignOutV2_Pre,
@@ -578,6 +608,14 @@ abstract class ApiToServiceBase extends $grpc.Service {
     return toSignInByAppToken(call, await request);
   }
 
+  $async.Future<$0.QrCodeInfo> toGetSignInQrCode_Pre($grpc.ServiceCall call, $async.Future<$0.DefaultRequest> request) async {
+    return toGetSignInQrCode(call, await request);
+  }
+
+  $async.Future<$0.SignInReply> toQrCodeSignIn_Pre($grpc.ServiceCall call, $async.Future<$0.QrCodeInfo> request) async {
+    return toQrCodeSignIn(call, await request);
+  }
+
   $async.Future<$0.DefaultReply> toSignOutV2_Pre($grpc.ServiceCall call, $async.Future<$0.DefaultRequest> request) async {
     return toSignOutV2(call, await request);
   }
@@ -706,6 +744,8 @@ abstract class ApiToServiceBase extends $grpc.Service {
   $async.Future<$0.GetSmsSendLeftTimeReply> toGetSmsSendLeftTimeV2($grpc.ServiceCall call, $0.GetSmsSendLeftTimeRequest request);
   $async.Future<$0.SignInReply> toSignInV2($grpc.ServiceCall call, $0.ToSignInRequestV2 request);
   $async.Future<$0.SignInReply> toSignInByAppToken($grpc.ServiceCall call, $1.ToSignInByAppTokenRequest request);
+  $async.Future<$0.QrCodeInfo> toGetSignInQrCode($grpc.ServiceCall call, $0.DefaultRequest request);
+  $async.Future<$0.SignInReply> toQrCodeSignIn($grpc.ServiceCall call, $0.QrCodeInfo request);
   $async.Future<$0.DefaultReply> toSignOutV2($grpc.ServiceCall call, $0.DefaultRequest request);
   $async.Future<$0.GetServerTimeRp> toGetServerTime($grpc.ServiceCall call, $0.DefaultRequest request);
   $async.Future<$0.ToGetUpdateVersionReply> toGetUpdateVersion($grpc.ServiceCall call, $0.ToGetUpdateVersionRequest request);

+ 19 - 0
app_business/lib/view/login.dart

@@ -1,3 +1,6 @@
+import 'dart:convert';
+
+import 'package:app_business/generated/base.pb.dart' as pb;
 import 'package:app_business/service/api.dart';
 import 'package:get/get.dart';
 import 'package:track_common/view.dart';
@@ -19,4 +22,20 @@ class LoginControllerImp extends LoginController {
   Future<void> signIn(String phone, String code) async {
     return _api.signIn(phone, code, '');
   }
+
+  @override
+  Future<(String, String)> getQrCode() async {
+    final r = await _api.stub.toGetSignInQrCode(DefaultRequest());
+    final json = const JsonEncoder()
+        .convert({'KeyType': 'BusinessLogin', 'code': r.qrCode});
+    return (json, r.qrCode);
+  }
+
+  @override
+  Future<bool> isQrCodeOk(String codeValue) async {
+    final r =
+        await _api.stub.toQrCodeSignIn(pb.QrCodeInfo()..qrCode = qrCodeValue);
+    _api.token = r.token;
+    return true;
+  }
 }

BIN
libs/track_common/assets/images/ic_login_qrcode.png


+ 1 - 0
libs/track_common/lib/generated/assets.dart

@@ -10,6 +10,7 @@ class Assets {
   static const String imagesIcCp = 'assets/images/ic_cp.png';
   static const String imagesIcLocation = 'assets/images/ic_location.png';
   static const String imagesIcLoginLogo = 'assets/images/ic_login_logo.png';
+  static const String imagesIcLoginQrcode = 'assets/images/ic_login_qrcode.png';
   static const String imagesIcMapScale = 'assets/images/ic_map_scale.png';
   static const String imagesIcNoData = 'assets/images/ic_no_data.png';
   static const String imagesImCompassNoMap =

+ 68 - 6
libs/track_common/lib/view/login/login_controller.dart

@@ -15,18 +15,64 @@ abstract class LoginController extends GetxController {
   Timer? ticker;
   var isSignInEnable = true;
   final isAgreeContract = false.obs;
+  final isPageQrCode = false.obs;
+  final qrCode = ''.obs;
+  var qrCodeValue = '';
+  late final Object signOkCall;
+  var codeErr = ''.obs;
 
   Future<Duration> getCodeLifeTime(String phone);
   Future<void> authSendCodeToPhone(String phone);
   Future<void> signIn(String phone, String code);
 
+  /// (json, codeValue)
+  Future<(String, String)> getQrCode();
+  Future<bool> isQrCodeOk(String codeValue);
+
   @override
   void onInit() {
+    signOkCall = Get.arguments;
+
     super.onInit();
 
     ticker = Timer.periodic(1.seconds, (timer) {
       codeRetryLeft.value -= 1.seconds;
     });
+
+    workCheckQrCode();
+  }
+
+  void flushQrCode() async {
+    codeErr.value = '';
+    final code = await getQrCode();
+    debug('qr code: $code');
+    qrCode.value = code.$1;
+    qrCodeValue = code.$2;
+  }
+
+  void toQrcode() async {
+    isPageQrCode.value = true;
+    flushQrCode();
+  }
+
+  void workCheckQrCode() async {
+    while (!isClosed) {
+      await 1.seconds.delay();
+      if (!isPageQrCode.value || qrCodeValue.isEmpty || codeErr.isNotEmpty) {
+        continue;
+      }
+
+      try {
+        if (await isQrCodeOk(qrCodeValue)) {
+          onSignOk();
+          return;
+        }
+      } on GrpcError catch (e) {
+        codeErr.value = e.message ?? '未知错误';
+      } catch (e) {
+        warn('code err:', e);
+      }
+    }
   }
 
   void onGetCode() async {
@@ -54,6 +100,15 @@ abstract class LoginController extends GetxController {
         });
   }
 
+  void onSignOk() {
+    final call = signOkCall;
+    if (call is VoidCallback) {
+      call();
+    } else {
+      Get.back(result: true, closeOverlays: true);
+    }
+  }
+
   Future<void> onSignIn() async {
     // if (!isAgreeContract.value) {
     //   Get.snackbar('请先', '阅读并同意协议');
@@ -79,13 +134,8 @@ abstract class LoginController extends GetxController {
           () async {
             info("登录 ${phone.value} ${password.value}");
             await signIn(phone.value, password.value);
-            final call = Get.arguments;
             info("登录成功");
-            if (call is VoidCallback) {
-              call();
-            } else {
-              Get.back(result: true, closeOverlays: true);
-            }
+            onSignOk();
           },
           errTitle: '登录失败',
           onError: (e) async {
@@ -130,4 +180,16 @@ class LoginControllerMock extends LoginController {
     // TODO: implement signIn
     throw UnimplementedError();
   }
+
+  @override
+  Future<(String, String)> getQrCode() {
+    // TODO: implement getQrCode
+    throw UnimplementedError();
+  }
+
+  @override
+  Future<bool> isQrCodeOk(String codeValue) {
+    // TODO: implement isQrCodeOk
+    throw UnimplementedError();
+  }
 }

+ 161 - 83
libs/track_common/lib/view/login/login_view.dart

@@ -1,6 +1,7 @@
 import 'package:common_pub/screen.dart';
 import 'package:common_pub/utils.dart';
 import 'package:flutter/gestures.dart';
+import 'package:pretty_qr_code/pretty_qr_code.dart';
 import 'package:rive/rive.dart';
 import 'package:track_common/widget/prelude.dart';
 
@@ -66,7 +67,6 @@ class LoginView extends GetView<LoginController> {
     }
 
     return Container(
-        // margin: EdgeInsets.fromLTRB(context.width*(6), context.width*5.6), context.width*(6), context.width*(2)),
         padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
         decoration: const BoxDecoration(
           // color: Color(0xffffcb00),
@@ -79,51 +79,25 @@ class LoginView extends GetView<LoginController> {
           mainAxisAlignment: MainAxisAlignment.center,
           crossAxisAlignment: CrossAxisAlignment.center,
           children: [
-            Spacer(),
-            Text(
+            const Spacer(),
+            const Text(
               '定向运动活动现场监控系统',
               textAlign: TextAlign.center,
               style: TextStyle(fontSize: 29.87, fontWeight: FontWeight.w500),
             ),
-            // Expanded(child: wInput(context)),
             SizedBox(height: inputHeight, child: wInput(context)),
             Image.asset(Assets.imagesIcLoginLogo, height: 24, package: package),
-            Spacer()
+            const Spacer()
             // Expanded(flex: 10,child:  wExtra()),
           ],
         ));
   }
 
-  InputDecoration wInputDecoration(hintText) {
-    return InputDecoration(
-      // isCollapsed: true,
-      contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
-      hintText: hintText,
-      // hintStyle: TextStyle(fontSize: 12.0.rpx),
-      focusedBorder: OutlineInputBorder(
-        borderRadius: BorderRadius.circular(4.0),
-        borderSide: const BorderSide(
-          color: Colors.blue,
-          width: 1.0,
-        ),
-      ),
-      enabledBorder: OutlineInputBorder(
-        borderRadius: BorderRadius.circular(4.0),
-        borderSide: const BorderSide(
-          color: Color(0xffaaaaaa),
-          width: 1.0,
-        ),
-      ),
-    );
-  }
-
   Widget wInput(BuildContext context) {
-    return Container(
+    return Obx(() => Container(
         width: 432.8,
         height: 424,
-        margin: EdgeInsets.only(top: 22.04, bottom: 21),
-        padding: EdgeInsets.fromLTRB(
-            context.wp(6), context.wp(3.6), context.wp(6), context.wp(2)),
+        margin: const EdgeInsets.only(top: 22.04, bottom: 21),
         decoration: BoxDecoration(
           color: const Color(0xffffffff),
           borderRadius: BorderRadius.circular(context.wp(1.0)),
@@ -134,55 +108,7 @@ class LoginView extends GetView<LoginController> {
                 blurRadius: 6)
           ],
         ),
-        child: Column(
-          children: [
-            // const Spacer(flex: 20),
-            TextFormField(
-                decoration: wInputDecoration('请输入手机号'),
-                validator: (String? value) {
-                  if (value == null || !value.isPhoneNumber) {
-                    return '请输入正确的手机号';
-                  }
-                  return null;
-                },
-                onChanged: (value) {
-                  controller.phone.value = value;
-                },
-                keyboardType: TextInputType.phone),
-            const Spacer(flex: 50),
-            Stack(
-              alignment: Alignment.centerRight,
-              children: [
-                TextFormField(
-                  decoration: wInputDecoration('请输入验证码'),
-                  onChanged: (value) {
-                    controller.password.value = value;
-                  },
-                  keyboardType: TextInputType.number,
-                ),
-                Padding(
-                  padding: const EdgeInsets.only(right: 4),
-                  child: SizedBox(
-                    width: 84,
-                    height: 37,
-                    child: Obx(() => GetCodeButton(
-                          codeRetryLeft: controller.codeRetryLeft.value,
-                          onPressed: () => controller.onGetCode(),
-                        )),
-                  ),
-                ),
-              ],
-            ),
-            // _TextContract(),
-            const Spacer(flex: 80),
-            button(
-              context,
-              '登    录',
-              () => controller.onSignIn(),
-            ),
-            const Spacer(flex: 10),
-          ],
-        ));
+        child: controller.isPageQrCode.value ? _InputQrCode() : _InputPhone()));
   }
 
   Widget wExtra() {
@@ -371,8 +297,6 @@ class AnimatedColors extends AnimatedWidget {
   @override
   Widget build(BuildContext context) {
     final animation = listenable as Animation<double>;
-    var style = (context.textTheme.titleLarge ?? const TextStyle())
-        .copyWith(color: Colors.white, fontSize: context.wp(8.33));
 
     var children = <Widget>[];
 
@@ -413,3 +337,157 @@ class AnimatedColors extends AnimatedWidget {
     );
   }
 }
+
+class _InputPhone extends GetView<LoginController> {
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        Align(
+            alignment: Alignment.topRight,
+            child: GestureDetector(
+              onTap: () => controller.toQrcode(),
+              child: Image.asset(Assets.imagesIcLoginQrcode,
+                  package: package, width: 58.5, height: 58.5),
+            )),
+        Padding(
+            padding: EdgeInsets.fromLTRB(
+                context.wp(6), context.wp(3.6), context.wp(6), context.wp(2)),
+            child: Column(
+              children: [
+                // const Spacer(flex: 20),
+                TextFormField(
+                    decoration: wInputDecoration('请输入手机号'),
+                    validator: (String? value) {
+                      if (value == null || !value.isPhoneNumber) {
+                        return '请输入正确的手机号';
+                      }
+                      return null;
+                    },
+                    onChanged: (value) {
+                      controller.phone.value = value;
+                    },
+                    keyboardType: TextInputType.phone),
+                const Spacer(flex: 50),
+                Stack(
+                  alignment: Alignment.centerRight,
+                  children: [
+                    TextFormField(
+                      decoration: wInputDecoration('请输入验证码'),
+                      onChanged: (value) {
+                        controller.password.value = value;
+                      },
+                      keyboardType: TextInputType.number,
+                    ),
+                    Padding(
+                      padding: const EdgeInsets.only(right: 4),
+                      child: SizedBox(
+                        width: 84,
+                        height: 37,
+                        child: Obx(() => GetCodeButton(
+                              codeRetryLeft: controller.codeRetryLeft.value,
+                              onPressed: () => controller.onGetCode(),
+                            )),
+                      ),
+                    ),
+                  ],
+                ),
+                // _TextContract(),
+                const Spacer(flex: 80),
+                button(
+                  context,
+                  '登    录',
+                  () => controller.onSignIn(),
+                ),
+                const Spacer(flex: 10),
+              ],
+            ))
+      ],
+    );
+  }
+
+  InputDecoration wInputDecoration(hintText) {
+    return InputDecoration(
+      // isCollapsed: true,
+      contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+      hintText: hintText,
+      // hintStyle: TextStyle(fontSize: 12.0.rpx),
+      focusedBorder: OutlineInputBorder(
+        borderRadius: BorderRadius.circular(4.0),
+        borderSide: const BorderSide(
+          color: Colors.blue,
+          width: 1.0,
+        ),
+      ),
+      enabledBorder: OutlineInputBorder(
+        borderRadius: BorderRadius.circular(4.0),
+        borderSide: const BorderSide(
+          color: Color(0xffaaaaaa),
+          width: 1.0,
+        ),
+      ),
+    );
+  }
+}
+
+class _InputQrCode extends GetView<LoginController> {
+  @override
+  Widget build(BuildContext context) {
+    return Obx(() => Column(
+          crossAxisAlignment: CrossAxisAlignment.center,
+          children: [
+            Row(
+              children: [
+                const Spacer(),
+                TextButton(
+                    onPressed: () => controller.isPageQrCode.value = false,
+                    child: const Text('使用短信登录',
+                        style: TextStyle(color: Color(0xff004670))))
+              ],
+            ),
+            Stack(children: [
+              GestureDetector(
+                  onTap: () => controller.flushQrCode(),
+                  child: Obx(() => SizedBox.square(
+                      dimension: 104,
+                      child:
+                          PrettyQrView.data(data: controller.qrCode.value)))),
+              if (controller.codeErr.value.isNotEmpty)
+                Container(
+                  height: 104,
+                  width: 104,
+                  color: const Color(0xe6ffffff),
+                  alignment: Alignment.center,
+                  child: Column(
+                    children: [
+                      const Expanded(
+                          child: Center(
+                        child: Icon(
+                          Icons.cancel,
+                          color: Colors.red,
+                          size: 53.3,
+                        ),
+                      )),
+                      Text(controller.codeErr.value,
+                          style: const TextStyle(
+                              fontSize: 14.2, fontWeight: FontWeight.w500)),
+                      const SizedBox(height: 5)
+                    ],
+                  ),
+                )
+            ]),
+            Expanded(
+                child: Center(
+              child: controller.codeErr.value.isEmpty
+                  ? const Text('请用彩图奔跑APP 扫一扫登录')
+                  : TextButton(
+                      onPressed: () => controller.flushQrCode(),
+                      child: const Text(
+                        '重新扫码',
+                        style: TextStyle(color: Color(0xff004670)),
+                      )),
+            ))
+          ],
+        ));
+  }
+}

+ 16 - 0
libs/track_common/pubspec.lock

@@ -447,6 +447,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.5.1"
+  pretty_qr_code:
+    dependency: "direct main"
+    description:
+      name: pretty_qr_code
+      sha256: "2163eb6e2231f3e8b2b2503d94f31d8bd6846bc428e9b5cf499c05fda0b34a16"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.1.0"
   protobuf:
     dependency: transitive
     description:
@@ -463,6 +471,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.6.0"
+  qr:
+    dependency: transitive
+    description:
+      name: qr
+      sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.0.1"
   rive:
     dependency: "direct main"
     description:

+ 1 - 1
libs/track_common/pubspec.yaml

@@ -35,7 +35,7 @@ dependencies:
   f_cache: ^3.1.1
   rive: ^0.12.3
   cached_network_image: ^3.3.0
-
+  pretty_qr_code: ^3.0.0
 dev_dependencies:
   flutter_test:
     sdk: flutter

+ 1 - 1
protos/app_api

@@ -1 +1 @@
-Subproject commit 53aedde50699b258abfa8867beee1ec25e97849c
+Subproject commit d01ee61cb5c1e04bafb1c3d9c6895086fc0ba29d