diff --git a/lib/features/auth/presentation/login_page.dart b/lib/features/auth/presentation/login_page.dart index 4243850..b566894 100644 --- a/lib/features/auth/presentation/login_page.dart +++ b/lib/features/auth/presentation/login_page.dart @@ -47,11 +47,19 @@ class LoginPage extends HookConsumerWidget { Future returnAfterLogin() async { final target = next?.trim(); final router = GoRouter.of(context); - if (target == null || target.isEmpty) { - router.go(AppRoutes.profile); + if (target != null && target.isNotEmpty) { + router.go(target); return; } - router.go(target); + // The mall opens this page with no `next`. Pop back to its resident + // WebView so the auth-state listener can deliver `enterWebMall` after + // the app-mall token has been issued. Replacing the route here disposes + // that WebView before the asynchronous hand-off can finish. + if (context.canPop()) { + context.pop(); + } else { + router.go(AppRoutes.profile); + } } void startCountdown() { diff --git a/lib/features/mall/data/mall_auth_api.dart b/lib/features/mall/data/mall_auth_api.dart index c32d271..02281d8 100644 --- a/lib/features/mall/data/mall_auth_api.dart +++ b/lib/features/mall/data/mall_auth_api.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -25,17 +27,25 @@ class MallAuthApi { Future issueAppMallAuthToken() async { final response = await _dio.post>( '/api/v1/auth/mall/app-token', - data: const [], + // Dio serializes a List as the UTF-8 text "[]". This endpoint + // expects an empty protobuf message, whose wire representation is zero + // bytes; Uint8List keeps the request body binary and untransformed. + data: Uint8List(0), options: protobufOptions(), ); final reply = ProtoMessage(response.data ?? const []); final token = reply.string(1).trim(); - if (token.isEmpty) throw const FormatException('Missing mall auth token'); + final expiresIn = reply.integer(2); + final sourceApp = reply.string(3).trim(); + final scene = reply.string(4).trim(); + if (token.isEmpty || expiresIn <= 0 || sourceApp.isEmpty || scene.isEmpty) { + throw const FormatException('Incomplete mall auth token response'); + } return AppMallAuthToken( authToken: token, - expiresIn: reply.integer(2), - sourceApp: reply.string(3), - scene: reply.string(4), + expiresIn: expiresIn, + sourceApp: sourceApp, + scene: scene, ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 251c824..157c243 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: jinzhi_flutter_app description: "金值 - 贵金属资产管家" publish_to: 'none' -version: 1.0.0+1 +version: 1.3.0+1 environment: sdk: ^3.9.2 diff --git a/test/features/mall/mall_auth_api_test.dart b/test/features/mall/mall_auth_api_test.dart new file mode 100644 index 0000000..8ab0f6a --- /dev/null +++ b/test/features/mall/mall_auth_api_test.dart @@ -0,0 +1,92 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jinzhi_flutter_app/features/mall/data/mall_auth_api.dart'; + +void main() { + test('sends an empty protobuf body when issuing a mall auth token', () async { + Object? requestData; + String? contentType; + final dio = Dio() + ..interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) { + requestData = options.data; + contentType = options.contentType; + handler.resolve( + Response>( + requestOptions: options, + statusCode: 200, + data: _validResponse(), + ), + ); + }, + ), + ); + + final token = await MallAuthApi(dio).issueAppMallAuthToken(); + + expect(requestData, isA()); + expect(requestData, isEmpty); + expect(contentType, 'application/x-protobuf'); + expect(token.authToken, 'mall-token'); + expect(token.expiresIn, 300); + expect(token.sourceApp, 'jinzhi'); + expect(token.scene, 'web_mall'); + }); + + test('rejects an incomplete mall auth token response', () async { + final dio = Dio() + ..interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) { + handler.resolve( + Response>( + requestOptions: options, + statusCode: 200, + data: _stringField(1, 'mall-token'), + ), + ); + }, + ), + ); + + await expectLater( + MallAuthApi(dio).issueAppMallAuthToken(), + throwsFormatException, + ); + }); +} + +List _validResponse() => [ + ..._stringField(1, 'mall-token'), + ..._integerField(2, 300), + ..._stringField(3, 'jinzhi'), + ..._stringField(4, 'web_mall'), +]; + +List _stringField(int field, String value) { + final bytes = utf8.encode(value); + return [ + ..._varint((field << 3) | 2), + ..._varint(bytes.length), + ...bytes, + ]; +} + +List _integerField(int field, int value) => [ + ..._varint(field << 3), + ..._varint(value), +]; + +List _varint(int value) { + final bytes = []; + while (value > 0x7f) { + bytes.add((value & 0x7f) | 0x80); + value >>= 7; + } + bytes.add(value); + return bytes; +}