From b8f1f82ee5313137205e41cb56874b3797bf3748 Mon Sep 17 00:00:00 2001 From: pp-bot Date: Tue, 31 Mar 2026 15:36:54 +0900 Subject: [PATCH] =?UTF-8?q?feat(login):=20=E4=BA=8C=E7=BA=A7=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E7=99=BB=E5=BD=95=E6=94=AF=E6=8C=81=EF=BC=88STATUS=5F?= =?UTF-8?q?SECONDARY=5FPASSCODE=5FERROR=20#1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题 旧版 Flutter 项目在 /vcode/check 返回 30164 时展示二级密码输入界面; 新版完全缺失此路径,有二级密码的账号无法登录。 ## 改动 ### networks_sdk - `networks_sdk_method_channel_datasource.dart`:executeRequest 的 generic catch 改为 rethrow,允许 decodeResponse override 抛出 自定义业务异常(原为 ApiError.unknown 包裹导致数据丢失) ### 数据层 - `errors.dart`:新增 `secondaryPasscodeRequired = 30164` - `exceptions.dart`(新增):`SecondaryPasscodeRequiredException` 携带 vcodeToken / recoveryEmail / hint / resetStatus - `verify_otp_request.dart`:override decodeResponse,拦截 30164, 从响应 data 提取字段,throw SecondaryPasscodeRequiredException - `login_request.dart`:新增可选 password 字段 + toJson override (条件序列化,null 时不带 password 字段) - `auth_repository.dart`:新增 loginWithPasscode() 接口 - `auth_repository_impl.dart`:实现 loginWithPasscode() ### 业务层 - `login_usecase.dart`:新增 loginWithSecondaryPasscode() (MD5 哈希 passcode → 调 AuthRepository.loginWithPasscode) - `pubspec.yaml`:新增 crypto: ^3.0.6(用于 MD5) ### UI 层 - `login_state.dart`:新增 LoginStep.secondaryPasscode + vcodeToken / passcodeHint / recoveryEmail 字段 - `login_view_model.dart`:verifyAndLogin 捕获 SecondaryPasscodeRequiredException 跳转步骤 3;新增 verifyPasscode() - `login_secondary_passcode_step.dart`(新增):密码输入 UI(hint 显示、 obscured 输入框、错误提示、忘记密码占位) - `login_page.dart`:switch 路由接入 LoginStep.secondaryPasscode --- apps/im_app/lib/core/foundation/errors.dart | 5 + .../lib/core/foundation/exceptions.dart | 32 ++++ .../im_app/lib/data/remote/login_request.dart | 23 +++ .../lib/data/remote/verify_otp_request.dart | 28 ++++ .../repositories/auth_repository_impl.dart | 25 +++ .../domain/repositories/auth_repository.dart | 11 ++ .../login/presentation/login_state.dart | 25 ++- .../login/presentation/login_view_model.dart | 46 +++++ .../login/usecases/login_usecase.dart | 45 +++++ .../lib/features/login/view/login_page.dart | 47 ++++-- .../login_secondary_passcode_step.dart | 158 ++++++++++++++++++ apps/im_app/pubspec.yaml | 1 + ...etworks_sdk_method_channel_datasource.dart | 7 +- 13 files changed, 438 insertions(+), 15 deletions(-) create mode 100644 apps/im_app/lib/core/foundation/exceptions.dart create mode 100644 apps/im_app/lib/features/login/view/widgets/login_secondary_passcode_step.dart diff --git a/apps/im_app/lib/core/foundation/errors.dart b/apps/im_app/lib/core/foundation/errors.dart index fff0c69..2a001aa 100644 --- a/apps/im_app/lib/core/foundation/errors.dart +++ b/apps/im_app/lib/core/foundation/errors.dart @@ -41,4 +41,9 @@ class ApiErrorCodes { /// 触发图片验证:data 含各平台 CAPTCHA token(android / ios / web) static const int captchaRequired = 30174; + // ── 二级密码(30164)── + + /// 账号已设置二级密码,需要用户输入后携带 MD5 哈希调 login-user + /// data 含 vcode_token / recovery_email / hint / reset_status + static const int secondaryPasscodeRequired = 30164; } diff --git a/apps/im_app/lib/core/foundation/exceptions.dart b/apps/im_app/lib/core/foundation/exceptions.dart new file mode 100644 index 0000000..201cadd --- /dev/null +++ b/apps/im_app/lib/core/foundation/exceptions.dart @@ -0,0 +1,32 @@ +/// 自定义业务异常 +/// +/// 集中管理需要在 UseCase / Repository 层抛出并在 UI 层捕获的 +/// 非 ApiError 业务异常。 + +/// 服务端要求输入二级密码(错误码 30164) +/// +/// 当 `/vcode/check` 返回 30164 时,服务端同时下发: +/// - [vcodeToken] — 本次验证会话令牌(后续调 login-user 须携带) +/// - [recoveryEmail] — 找回密码用的脱敏邮箱 +/// - [hint] — 用户设置的二级密码提示语 +/// - [resetStatus] — 是否可重置(true = 可走找回流程) +/// +/// 上层 catch 此异常后跳转二级密码输入界面, +/// 成功后调 [AuthRepository.loginWithPasscode] 完成登录。 +class SecondaryPasscodeRequiredException implements Exception { + final String vcodeToken; + final String recoveryEmail; + final String hint; + final bool resetStatus; + + const SecondaryPasscodeRequiredException({ + required this.vcodeToken, + required this.recoveryEmail, + required this.hint, + required this.resetStatus, + }); + + @override + String toString() => + 'SecondaryPasscodeRequiredException(vcodeToken=$vcodeToken, hint=$hint)'; +} diff --git a/apps/im_app/lib/data/remote/login_request.dart b/apps/im_app/lib/data/remote/login_request.dart index e5a1c85..8854938 100644 --- a/apps/im_app/lib/data/remote/login_request.dart +++ b/apps/im_app/lib/data/remote/login_request.dart @@ -182,9 +182,32 @@ class LoginRequest extends ApiRequestable @JsonKey(name: 'vcode_token') final String vcodeToken; + /// 二级密码(MD5 哈希后的值)。 + /// 账号未设置二级密码时传 null,字段不序列化到请求体。 + /// 对齐旧版:`accountLogin({String? password})` → 有值才加入 dataBody + @JsonKey(name: 'password', includeToJson: false) // 由下方 toJson() 手动控制 + final String? password; + LoginRequest({ required this.countryCode, required this.contact, required this.vcodeToken, + this.password, }); + + /// 手动 override toJson() 以支持 password 条件序列化。 + /// 类的 override 优先于 mixin(_$LoginRequestApi.toJson), + /// 即使 build_runner 重新生成 .g.dart 也不影响此行为。 + @override + Map toJson() { + final map = { + 'country_code': countryCode, + 'contact': contact, + 'vcode_token': vcodeToken, + }; + if (password != null) { + map['password'] = password; + } + return map; + } } diff --git a/apps/im_app/lib/data/remote/verify_otp_request.dart b/apps/im_app/lib/data/remote/verify_otp_request.dart index 2c4d6bc..0fa0471 100644 --- a/apps/im_app/lib/data/remote/verify_otp_request.dart +++ b/apps/im_app/lib/data/remote/verify_otp_request.dart @@ -1,7 +1,10 @@ +import 'package:dio/dio.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:networks_sdk/networks_sdk.dart'; import 'package:im_app/core/foundation/api_paths.dart'; +import 'package:im_app/core/foundation/errors.dart'; +import 'package:im_app/core/foundation/exceptions.dart'; part 'verify_otp_request.g.dart'; @@ -61,4 +64,29 @@ class VerifyOtpRequest extends ApiRequestable this.email = '', this.type = 1, }); + + /// 拦截二级密码错误码 30164,将服务端 data 中的 vcode_token 等字段 + /// 包装为 [SecondaryPasscodeRequiredException] 抛出,供上层导航至 + /// 二级密码输入界面。其余情况委托给基类处理。 + @override + VerifyOtpResponse? decodeResponse(Response response) { + if (response.data is Map) { + final json = response.data as Map; + final rawCode = json['code']; + final code = rawCode is int + ? rawCode + : int.tryParse(rawCode?.toString() ?? '') ?? 0; + + if (code == ApiErrorCodes.secondaryPasscodeRequired) { + final data = json['data'] as Map?; + throw SecondaryPasscodeRequiredException( + vcodeToken: data?['vcode_token'] as String? ?? '', + recoveryEmail: data?['recovery_email'] as String? ?? '', + hint: data?['hint'] as String? ?? '', + resetStatus: data?['reset_status'] as bool? ?? false, + ); + } + } + return super.decodeResponse(response); + } } diff --git a/apps/im_app/lib/data/repositories/auth_repository_impl.dart b/apps/im_app/lib/data/repositories/auth_repository_impl.dart index 8194e4c..eb2beff 100644 --- a/apps/im_app/lib/data/repositories/auth_repository_impl.dart +++ b/apps/im_app/lib/data/repositories/auth_repository_impl.dart @@ -94,6 +94,31 @@ class AuthRepositoryImpl implements AuthRepository { return response.toEntity(); } + @override + Future loginWithPasscode({ + required String countryCode, + required String contact, + required String vcodeToken, + required String passwordMd5, + }) async { + final response = await _client.executeRequest( + LoginRequest( + countryCode: countryCode, + contact: contact, + vcodeToken: vcodeToken, + password: passwordMd5, + ), + ); + + if (response == null) { + throw Exception('Login with passcode failed: empty response'); + } + + _onTokenUpdate(response.accessToken); + + return response.toEntity(); + } + @override Future logout() async { await _client.executeRequest(LogoutRequest()); diff --git a/apps/im_app/lib/domain/repositories/auth_repository.dart b/apps/im_app/lib/domain/repositories/auth_repository.dart index 2940615..5b8dfb7 100644 --- a/apps/im_app/lib/domain/repositories/auth_repository.dart +++ b/apps/im_app/lib/domain/repositories/auth_repository.dart @@ -39,6 +39,17 @@ abstract interface class AuthRepository { required String vcodeToken, }); + /// 二级密码登录 — 携带 MD5 哈希后的密码完成登录 + /// + /// 在 [verifyOtp] 抛出 [SecondaryPasscodeRequiredException] 后使用: + /// 用户输入二级密码 → 上层 MD5 → 调此方法 → POST login-user with password。 + Future loginWithPasscode({ + required String countryCode, + required String contact, + required String vcodeToken, + required String passwordMd5, + }); + /// 退出登录 Future logout(); } diff --git a/apps/im_app/lib/features/login/presentation/login_state.dart b/apps/im_app/lib/features/login/presentation/login_state.dart index fa6ba30..aca418a 100644 --- a/apps/im_app/lib/features/login/presentation/login_state.dart +++ b/apps/im_app/lib/features/login/presentation/login_state.dart @@ -5,6 +5,9 @@ enum LoginStep { /// 步骤 2:输入验证码 otp, + + /// 步骤 3(可选):输入二级密码(账号已设置时触发) + secondaryPasscode, } /// 登录页面状态(手动 copyWith) @@ -18,15 +21,18 @@ class LoginState { this.contact = '', this.isLoading = false, this.error, + this.vcodeToken = '', + this.passcodeHint = '', + this.recoveryEmail = '', }); - /// 当前步骤(手机号输入 or 验证码输入) + /// 当前步骤(手机号输入 / 验证码输入 / 二级密码输入) final LoginStep step; /// 国家代码(默认 +65,暂不支持切换) final String countryCode; - /// 已提交的手机号(步骤 2 用于显示和构建请求) + /// 已提交的手机号(步骤 2 / 3 用于显示和构建请求) final String contact; /// 是否正在请求中 @@ -35,6 +41,15 @@ class LoginState { /// 错误信息(null = 无错误) final String? error; + /// vcode_token(OTP 验证通过后服务端下发,二级密码步骤需携带) + final String vcodeToken; + + /// 用户设置的二级密码提示语(步骤 3 显示) + final String passcodeHint; + + /// 脱敏找回邮箱(步骤 3 "忘记密码" 提示用) + final String recoveryEmail; + LoginState copyWith({ LoginStep? step, String? countryCode, @@ -42,6 +57,9 @@ class LoginState { bool? isLoading, String? error, bool clearError = false, + String? vcodeToken, + String? passcodeHint, + String? recoveryEmail, }) { return LoginState( step: step ?? this.step, @@ -49,6 +67,9 @@ class LoginState { contact: contact ?? this.contact, isLoading: isLoading ?? this.isLoading, error: clearError ? null : (error ?? this.error), + vcodeToken: vcodeToken ?? this.vcodeToken, + passcodeHint: passcodeHint ?? this.passcodeHint, + recoveryEmail: recoveryEmail ?? this.recoveryEmail, ); } diff --git a/apps/im_app/lib/features/login/presentation/login_view_model.dart b/apps/im_app/lib/features/login/presentation/login_view_model.dart index b26245c..fd2cb9a 100644 --- a/apps/im_app/lib/features/login/presentation/login_view_model.dart +++ b/apps/im_app/lib/features/login/presentation/login_view_model.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:networks_sdk/networks_sdk.dart'; import 'package:im_app/app/di/app_providers.dart'; +import 'package:im_app/core/foundation/exceptions.dart'; import 'package:im_app/features/login/di/auth_providers.dart'; import 'package:im_app/features/login/presentation/login_state.dart'; @@ -51,6 +52,9 @@ class LoginViewModel extends Notifier { } /// 步骤 2+3:校验验证码并完成登录 + /// + /// 若账号已设置二级密码,服务端返回 30164, + /// 此方法捕获 [SecondaryPasscodeRequiredException] 并跳转到步骤 3。 Future verifyAndLogin(String code) async { if (state.isLoading) return; state = state.copyWith(isLoading: true, error: null); @@ -64,6 +68,48 @@ class LoginViewModel extends Notifier { code: code, ); + if (!ref.mounted) return; + ref.read(authNotifierProvider).login(uid: user.uid); + } on SecondaryPasscodeRequiredException catch (e) { + // 账号已设置二级密码 → 跳转二级密码输入步骤 + if (!ref.mounted) return; + state = state.copyWith( + step: LoginStep.secondaryPasscode, + vcodeToken: e.vcodeToken, + passcodeHint: e.hint, + recoveryEmail: e.recoveryEmail, + isLoading: false, + clearError: true, + ); + } on FormatException catch (e) { + if (!ref.mounted) return; + state = state.copyWith(error: e.message, isLoading: false); + } on ApiError catch (e) { + if (!ref.mounted) return; + state = state.copyWith(error: e.displayMessage, isLoading: false); + } catch (e) { + if (!ref.mounted) return; + state = state.copyWith(error: e.toString(), isLoading: false); + } + } + + /// 步骤 3:用户输入二级密码后完成登录 + /// + /// [passcode] 为用户明文输入,UseCase 内部做 MD5 哈希。 + Future verifyPasscode(String passcode) async { + if (state.isLoading) return; + state = state.copyWith(isLoading: true, error: null); + + try { + final user = await ref + .read(loginUseCaseProvider) + .loginWithSecondaryPasscode( + countryCode: state.countryCode, + contact: state.contact, + vcodeToken: state.vcodeToken, + passcode: passcode, + ); + if (!ref.mounted) return; ref.read(authNotifierProvider).login(uid: user.uid); } on FormatException catch (e) { diff --git a/apps/im_app/lib/features/login/usecases/login_usecase.dart b/apps/im_app/lib/features/login/usecases/login_usecase.dart index 2a28d50..1d9e104 100644 --- a/apps/im_app/lib/features/login/usecases/login_usecase.dart +++ b/apps/im_app/lib/features/login/usecases/login_usecase.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; import 'package:networks_sdk/networks_sdk.dart'; import 'package:storage_sdk/storage_sdk.dart'; @@ -115,6 +118,48 @@ class LoginUseCase { return user; } + /// 步骤 2b:用户输入二级密码后完成登录 + /// + /// 在 [verifyAndLogin] 抛出 [SecondaryPasscodeRequiredException] 后调用: + /// - MD5 哈希用户输入的密码 + /// - 调 [AuthRepository.loginWithPasscode] + /// - 初始化 WebSocket / 数据库(与 [verifyAndLogin] 后半段一致) + /// + /// 抛出: + /// - [FormatException] — 密码为空 + /// - [ApiError] — 网络/服务端错误(密码错误 → 服务端返回错误码) + Future loginWithSecondaryPasscode({ + required String countryCode, + required String contact, + required String vcodeToken, + required String passcode, + }) async { + if (passcode.trim().isEmpty) { + throw const FormatException('密码不能为空'); + } + + // MD5 哈希(对齐旧版 makeMD5 逻辑) + final bytes = utf8.encode(passcode); + final passwordMd5 = md5.convert(bytes).toString(); + + final user = await _authRepository.loginWithPasscode( + countryCode: countryCode, + contact: contact, + vcodeToken: vcodeToken, + passwordMd5: passwordMd5, + ); + + final token = _apiConfig.token; + if (token != null && token.isNotEmpty) { + await _socketManager.connect(token: token); + } + + await _storageLifeCycle.openDatabase(user.uid); + await _userRepository.insertOrReplaceUser(user); + + return user; + } + void _validatePhone(String contact) { final trimmed = contact.trim(); if (trimmed.isEmpty) { diff --git a/apps/im_app/lib/features/login/view/login_page.dart b/apps/im_app/lib/features/login/view/login_page.dart index 6f40ce2..45b7eb0 100644 --- a/apps/im_app/lib/features/login/view/login_page.dart +++ b/apps/im_app/lib/features/login/view/login_page.dart @@ -5,6 +5,7 @@ import 'package:im_app/features/login/presentation/login_state.dart'; import 'package:im_app/features/login/presentation/login_view_model.dart'; import 'package:im_app/features/login/view/widgets/login_otp_step.dart'; import 'package:im_app/features/login/view/widgets/login_phone_step.dart'; +import 'package:im_app/features/login/view/widgets/login_secondary_passcode_step.dart'; /// 登录页 — 两步流程:手机号 → 验证码 /// @@ -24,11 +25,13 @@ class _LoginPageState extends ConsumerState { // demo 预填,上线前去掉 final _phoneCtrl = TextEditingController(text: '83465308'); final _otpCtrl = TextEditingController(text: '0000'); + final _passcodeCtrl = TextEditingController(); @override void dispose() { _phoneCtrl.dispose(); _otpCtrl.dispose(); + _passcodeCtrl.dispose(); super.dispose(); } @@ -44,8 +47,15 @@ class _LoginPageState extends ConsumerState { .verifyAndLogin(_otpCtrl.text.trim()); } + void _verifyPasscode() { + ref + .read(loginViewModelProvider.notifier) + .verifyPasscode(_passcodeCtrl.text); + } + void _backToPhone() { _otpCtrl.clear(); + _passcodeCtrl.clear(); ref.read(loginViewModelProvider.notifier).backToPhone(); } @@ -53,22 +63,35 @@ class _LoginPageState extends ConsumerState { Widget build(BuildContext context) { final state = ref.watch(loginViewModelProvider); + Widget body; + switch (state.step) { + case LoginStep.phone: + body = LoginPhoneStep( + phoneCtrl: _phoneCtrl, + state: state, + onSendOtp: () => _sendOtp(state), + ); + case LoginStep.otp: + body = LoginOtpStep( + otpCtrl: _otpCtrl, + state: state, + onVerifyAndLogin: _verifyAndLogin, + onBackToPhone: _backToPhone, + ); + case LoginStep.secondaryPasscode: + body = LoginSecondaryPasscodeStep( + passcodeCtrl: _passcodeCtrl, + state: state, + onVerifyPasscode: _verifyPasscode, + onBackToPhone: _backToPhone, + ); + } + return Scaffold( appBar: AppBar(automaticallyImplyLeading: false, title: const Text('登录')), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), - child: state.step == LoginStep.phone - ? LoginPhoneStep( - phoneCtrl: _phoneCtrl, - state: state, - onSendOtp: () => _sendOtp(state), - ) - : LoginOtpStep( - otpCtrl: _otpCtrl, - state: state, - onVerifyAndLogin: _verifyAndLogin, - onBackToPhone: _backToPhone, - ), + child: SingleChildScrollView(child: body), ), ); } diff --git a/apps/im_app/lib/features/login/view/widgets/login_secondary_passcode_step.dart b/apps/im_app/lib/features/login/view/widgets/login_secondary_passcode_step.dart new file mode 100644 index 0000000..a83527a --- /dev/null +++ b/apps/im_app/lib/features/login/view/widgets/login_secondary_passcode_step.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; + +import 'package:im_app/features/login/presentation/login_state.dart'; + +/// 步骤 3:二级密码输入界面 +/// +/// 当 OTP 验证后服务端返回 30164 时触发,显示: +/// - 脱敏手机号(参考 otp 步骤) +/// - 密码提示语(用户设置的 hint,可为空) +/// - 密码输入框(obscured) +/// - 确认按钮 +/// - "忘记密码" 占位(TODO: 接入找回流程) +/// +/// 对齐旧版 Flutter `secondaryPasscodeLoginVerifyView` +class LoginSecondaryPasscodeStep extends StatelessWidget { + const LoginSecondaryPasscodeStep({ + super.key, + required this.passcodeCtrl, + required this.state, + required this.onVerifyPasscode, + required this.onBackToPhone, + }); + + final TextEditingController passcodeCtrl; + final LoginState state; + final VoidCallback onVerifyPasscode; + final VoidCallback onBackToPhone; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 48), + + // 标题 + Text( + '请输入二级密码', + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 8), + + // 脱敏手机号 + Text( + state.maskedContact, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withAlpha(153), + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 32), + + // 密码提示语(hint 非空时显示) + if (state.passcodeHint.isNotEmpty) ...[ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + size: 16, + color: theme.colorScheme.onSurface.withAlpha(153), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + '提示:${state.passcodeHint}', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withAlpha(153), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + + // 密码输入框 + TextField( + controller: passcodeCtrl, + obscureText: true, + keyboardType: TextInputType.visiblePassword, + textInputAction: TextInputAction.done, + onSubmitted: (_) => onVerifyPasscode(), + decoration: const InputDecoration( + labelText: '二级密码', + hintText: '请输入您的二级密码', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.lock_outline), + ), + ), + + const SizedBox(height: 8), + + // 错误提示 + if (state.error != null) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + state.error!, + style: TextStyle( + color: theme.colorScheme.error, + fontSize: 13, + ), + textAlign: TextAlign.center, + ), + ), + + const SizedBox(height: 16), + + // 确认按钮 + FilledButton( + onPressed: state.isLoading ? null : onVerifyPasscode, + child: state.isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('确认'), + ), + + const SizedBox(height: 12), + + // 忘记密码(TODO: 接入找回流程) + TextButton( + onPressed: null, // TODO: 跳转找回密码页面 + child: Text( + '忘记密码?', + style: TextStyle( + color: theme.colorScheme.primary.withAlpha(128), + ), + ), + ), + + const SizedBox(height: 8), + + // 返回 + TextButton( + onPressed: onBackToPhone, + child: const Text('返回手机号'), + ), + ], + ); + } +} diff --git a/apps/im_app/pubspec.yaml b/apps/im_app/pubspec.yaml index 28ad45b..3e3d517 100644 --- a/apps/im_app/pubspec.yaml +++ b/apps/im_app/pubspec.yaml @@ -114,6 +114,7 @@ dependencies: # 图片网络缓存 — 磁盘+内存双缓存(#57) cached_network_image: ^3.3.1 + crypto: ^3.0.6 # 图片保存到相册(#32) image_gallery_saver_plus: ^3.0.5 diff --git a/packages/networks_sdk/lib/src/data/datasources/networks_sdk_method_channel_datasource.dart b/packages/networks_sdk/lib/src/data/datasources/networks_sdk_method_channel_datasource.dart index 5958eb6..6ad57f9 100644 --- a/packages/networks_sdk/lib/src/data/datasources/networks_sdk_method_channel_datasource.dart +++ b/packages/networks_sdk/lib/src/data/datasources/networks_sdk_method_channel_datasource.dart @@ -107,7 +107,12 @@ class NetworksSdkMethodChannelDataSource { } on ApiError { rethrow; } catch (e) { - throw ApiError.unknown(e.toString()); + // decodeResponse() overrides may intentionally throw business exceptions + // (e.g. SecondaryPasscodeRequiredException on code 30164). + // JSON decoding FormatExceptions are already caught inside decodeResponse() + // and wrapped as ApiError.decodingError before reaching here. + // Re-throw so callers can handle typed business exceptions. + rethrow; } }