diff --git a/.gitignore b/.gitignore index b810f53..e029d39 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +test/hive diff --git a/lib/apis/passport_api.dart b/lib/apis/passport_api.dart index 549fa05..4f8f466 100644 --- a/lib/apis/passport_api.dart +++ b/lib/apis/passport_api.dart @@ -17,7 +17,9 @@ class PassportAPI { static const String _api = '${Urls.apiHost}/passport'; static const String _user = '$_api/user'; - static Future> login(String u, String p) { + /// Login failed: + /// {data: {captcha: , desc_url: , description: 帐号或密码错误, error_code: 1009}, message: error} + static Future> login(String u, String p) { return HttpUtil.fetchModel( FetchType.post, url: '$_user/login/', @@ -33,4 +35,13 @@ class PassportAPI { contentType: Headers.formUrlEncodedContentType, ); } + + /// TODO(shirne): refresh token + static Future> restore() { + return HttpUtil.fetchModel( + FetchType.post, + url: '$_user/refresh/', + contentType: Headers.formUrlEncodedContentType, + ); + } } diff --git a/lib/app.dart b/lib/app.dart index d203760..b0c1c9f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:oktoast/oktoast.dart'; import 'exports.dart'; @@ -73,38 +74,40 @@ class JJAppState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { return _buildOKToast( - child: MaterialApp( - theme: themeBy(brightness: Brightness.light), - darkTheme: themeBy(brightness: Brightness.dark), - initialRoute: Routes.splashPage.name, - navigatorKey: JJ.navigatorKey, - navigatorObservers: [ - JJ.routeObserver, - JJNavigatorObserver(), - ], - onGenerateTitle: (BuildContext c) => c.l10n.appTitle, - onGenerateRoute: (RouteSettings settings) => onGenerateRoute( - settings: settings, - getRouteSettings: getRouteSettings, - notFoundPageBuilder: () => Container( - alignment: Alignment.center, - color: Colors.black, - child: Text( - context.l10n.exceptionRouteNotFound( - settings.name ?? context.l10n.exceptionRouteUnknown, + child: ProviderScope( + child: MaterialApp( + theme: themeBy(brightness: Brightness.light), + darkTheme: themeBy(brightness: Brightness.dark), + initialRoute: Routes.splashPage.name, + navigatorKey: JJ.navigatorKey, + navigatorObservers: [ + JJ.routeObserver, + JJNavigatorObserver(), + ], + onGenerateTitle: (BuildContext c) => c.l10n.appTitle, + onGenerateRoute: (RouteSettings settings) => onGenerateRoute( + settings: settings, + getRouteSettings: getRouteSettings, + notFoundPageBuilder: () => Container( + alignment: Alignment.center, + color: Colors.black, + child: Text( + context.l10n.exceptionRouteNotFound( + settings.name ?? context.l10n.exceptionRouteUnknown, + ), + style: const TextStyle(color: Colors.white, inherit: false), ), - style: const TextStyle(color: Colors.white, inherit: false), ), ), - ), - localizationsDelegates: JJLocalizations.localizationsDelegates, - supportedLocales: JJLocalizations.supportedLocales, - scrollBehavior: _ScrollBehavior(), - builder: (BuildContext context, Widget? child) => Stack( - children: [ - _buildAnnotatedRegion(context, child!), - _buildBottomPaddingVerticalShield(context), - ], + localizationsDelegates: JJLocalizations.localizationsDelegates, + supportedLocales: JJLocalizations.supportedLocales, + scrollBehavior: _ScrollBehavior(), + builder: (BuildContext context, Widget? child) => Stack( + children: [ + _buildAnnotatedRegion(context, child!), + _buildBottomPaddingVerticalShield(context), + ], + ), ), ), ); diff --git a/lib/constants/providers.dart b/lib/constants/providers.dart new file mode 100644 index 0000000..196d7c3 --- /dev/null +++ b/lib/constants/providers.dart @@ -0,0 +1,70 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../models/data_model.dart'; +import '../models/repositories.dart'; +import '../utils/hive_util.dart'; + +const _tokenKey = 'token'; + +class TokenNotifier extends StateNotifier { + TokenNotifier(super.state); + + bool get isLogin => state.token.isNotEmpty; + String get token => state.token; + + void restore() { + final data = HiveUtil.box.get(_tokenKey); + if (data != null) { + state = data; + } + } + + void update(String token) { + state = UserAuthen( + token: token, + expireIn: 7200, + timestamp: DateTime.now().millisecond, + ); + saveState(); + } + + void logout() { + state = UserAuthen(); + saveState(); + } + + void saveState() { + if (state.token.isEmpty) { + HiveUtil.box.delete(_tokenKey); + } else { + HiveUtil.box.put(_tokenKey, state); + } + } +} + +class UserPassportNotifier extends StateNotifier { + UserPassportNotifier(super.state); + + void update(UserPassportModel data) { + state = data; + } +} + +final tokenProvider = StateNotifierProvider((ref) { + final userPassport = ref.watch(userProvider); + if (userPassport.isEmpty) { + return TokenNotifier(UserAuthen()); + } + return TokenNotifier( + UserAuthen( + token: userPassport.sessionKey, + expireIn: 7200, + timestamp: DateTime.now().millisecond, + ), + )..saveState(); +}); + +final userProvider = + StateNotifierProvider((ref) { + return UserPassportNotifier(const UserPassportModel.empty()); +}); diff --git a/lib/constants/themes.dart b/lib/constants/themes.dart index 8e9e682..7a0067e 100644 --- a/lib/constants/themes.dart +++ b/lib/constants/themes.dart @@ -63,9 +63,28 @@ ThemeData themeBy({ inputDecorationTheme: InputDecorationTheme( border: InputBorder.none, enabledBorder: InputBorder.none, - hintStyle: TextStyle(color: tg.iconColor), + hintStyle: TextStyle(color: tg.iconColor, height: 1.35), + prefixIconColor: tg.iconColor, ), scaffoldBackgroundColor: tg.backgroundColor, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: themeColorDark, + shape: const StadiumBorder(), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: themeColorDark.withAlpha(200), + shape: const StadiumBorder(), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: themeColorDark.withAlpha(200), + shape: const StadiumBorder(), + ), + ), textSelectionTheme: TextSelectionThemeData( cursorColor: tg.themeColor, selectionColor: tg.themeColor.withOpacity(.25), diff --git a/lib/exports.dart b/lib/exports.dart index 06801aa..763b628 100644 --- a/lib/exports.dart +++ b/lib/exports.dart @@ -11,6 +11,7 @@ export 'apis/passport_api.dart'; export 'apis/recommend_api.dart'; export 'apis/tag_api.dart'; export 'constants/constants.dart'; +export 'constants/providers.dart'; export 'constants/resources.dart'; export 'constants/screens.dart'; export 'constants/styles.dart'; @@ -30,12 +31,14 @@ export 'internals/urls.dart'; export 'l10n/gen/jj_localizations.dart'; export 'models/data_model.dart'; export 'models/loading_base.dart'; +export 'models/repositories.dart'; export 'models/response_model.dart'; export 'routes/juejin_routes.dart'; export 'routes/page_route.dart'; export 'utils/cache_util.dart'; export 'utils/device_util.dart'; export 'utils/haptic_util.dart'; +export 'utils/hive_util.dart'; export 'utils/http_util.dart'; export 'utils/log_util.dart'; export 'utils/package_util.dart'; diff --git a/lib/extensions/string_extension.dart b/lib/extensions/string_extension.dart index 25ccbf9..1c0080f 100644 --- a/lib/extensions/string_extension.dart +++ b/lib/extensions/string_extension.dart @@ -4,6 +4,12 @@ import 'package:flutter/widgets.dart' show Characters; +final mobileRegExp = RegExp(r'^1[3-9]\d{9}$'); +final emailRegExp = RegExp( + r'^[a-z][0-9a-z\-_ \.]*@([a-z0-9]+\.)+?[a-z]+$', + caseSensitive: false, +); + extension StringExtension on String { String get notBreak => Characters(this).join('\u{200B}'); @@ -15,6 +21,10 @@ extension StringExtension on String { String removeFirst(Pattern pattern, [int startIndex = 0]) => replaceFirst(pattern, '', startIndex); + + bool isMobile() => mobileRegExp.hasMatch(this); + + bool isEmail() => emailRegExp.hasMatch(this); } extension NullableStringExtension on String? { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ec95ee5..b6a2bf7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -78,6 +78,20 @@ "sortRecommend": "Recommend", "sortLatest": "Latest", + "loginTitle": "Sign in", + "loginSlogan": "Log in to experience more", + "hintUsername":"Phone/email", + "hintPassword":"Password", + "buttonSignIn":"Sign in", + "linkSignUp":"Create an account", + "linkRetrieve":"Fogot password?", + + "loginSuccess": "Login success", + + "needUsername": "Please enter a mobile number or email", + "incorectUsername": "Username must be a mobile number or email", + "needPassword": "Please enter your password", + "durationYears": "{many, plural, =1{1 year} other{{many} years}} ago", "@durationYears": { "placeholders": {"many": {"type": "int"}} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 5c0da42..8542638 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -66,6 +66,20 @@ "join": "加入", + "loginSuccess": "登录成功", + + "loginTitle": "登录", + "loginSlogan": "登录体验更多精彩", + "hintUsername":"手机号/邮箱", + "hintPassword":"密码", + "buttonSignIn":"登录", + "linkSignUp":"注册新用户", + "linkRetrieve":"找回密码", + + "needUsername": "请填写用户名", + "incorectUsername": "请填写手机号码或者邮箱", + "needPassword": "请填写密码", + "durationYears": "{many, plural, other{{many}年前}}", "durationMonths": "{many, plural, other{{many}月前}}", "durationDays": "{many, plural, other{{many}天前}}", diff --git a/lib/l10n/gen/jj_localizations.dart b/lib/l10n/gen/jj_localizations.dart index d8311fb..9b4cd75 100644 --- a/lib/l10n/gen/jj_localizations.dart +++ b/lib/l10n/gen/jj_localizations.dart @@ -405,6 +405,72 @@ abstract class JJLocalizations { /// **'Latest'** String get sortLatest; + /// No description provided for @loginTitle. + /// + /// In en, this message translates to: + /// **'Sign in'** + String get loginTitle; + + /// No description provided for @loginSlogan. + /// + /// In en, this message translates to: + /// **'Log in to experience more'** + String get loginSlogan; + + /// No description provided for @hintUsername. + /// + /// In en, this message translates to: + /// **'Phone/email'** + String get hintUsername; + + /// No description provided for @hintPassword. + /// + /// In en, this message translates to: + /// **'Password'** + String get hintPassword; + + /// No description provided for @buttonSignIn. + /// + /// In en, this message translates to: + /// **'Sign in'** + String get buttonSignIn; + + /// No description provided for @linkSignUp. + /// + /// In en, this message translates to: + /// **'Create an account'** + String get linkSignUp; + + /// No description provided for @linkRetrieve. + /// + /// In en, this message translates to: + /// **'Fogot password?'** + String get linkRetrieve; + + /// No description provided for @loginSuccess. + /// + /// In en, this message translates to: + /// **'Login success'** + String get loginSuccess; + + /// No description provided for @needUsername. + /// + /// In en, this message translates to: + /// **'Please enter a mobile number or email'** + String get needUsername; + + /// No description provided for @incorectUsername. + /// + /// In en, this message translates to: + /// **'Username must be a mobile number or email'** + String get incorectUsername; + + /// No description provided for @needPassword. + /// + /// In en, this message translates to: + /// **'Please enter your password'** + String get needPassword; + /// No description provided for @durationYears. /// /// In en, this message translates to: diff --git a/lib/l10n/gen/jj_localizations_en.dart b/lib/l10n/gen/jj_localizations_en.dart index ef40a0f..80b4def 100644 --- a/lib/l10n/gen/jj_localizations_en.dart +++ b/lib/l10n/gen/jj_localizations_en.dart @@ -172,6 +172,39 @@ class JJLocalizationsEn extends JJLocalizations { @override String get sortLatest => 'Latest'; + @override + String get loginTitle => 'Sign in'; + + @override + String get loginSlogan => 'Log in to experience more'; + + @override + String get hintUsername => 'Phone/email'; + + @override + String get hintPassword => 'Password'; + + @override + String get buttonSignIn => 'Sign in'; + + @override + String get linkSignUp => 'Create an account'; + + @override + String get linkRetrieve => 'Fogot password?'; + + @override + String get loginSuccess => 'Login success'; + + @override + String get needUsername => 'Please enter a mobile number or email'; + + @override + String get incorectUsername => 'Username must be a mobile number or email'; + + @override + String get needPassword => 'Please enter your password'; + @override String durationYears(int many) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/gen/jj_localizations_zh.dart b/lib/l10n/gen/jj_localizations_zh.dart index e7cc01b..2005a4c 100644 --- a/lib/l10n/gen/jj_localizations_zh.dart +++ b/lib/l10n/gen/jj_localizations_zh.dart @@ -172,6 +172,39 @@ class JJLocalizationsZh extends JJLocalizations { @override String get sortLatest => '最新'; + @override + String get loginTitle => '登录'; + + @override + String get loginSlogan => '登录体验更多精彩'; + + @override + String get hintUsername => '手机号/邮箱'; + + @override + String get hintPassword => '密码'; + + @override + String get buttonSignIn => '登录'; + + @override + String get linkSignUp => '注册新用户'; + + @override + String get linkRetrieve => '找回密码'; + + @override + String get loginSuccess => '登录成功'; + + @override + String get needUsername => '请填写用户名'; + + @override + String get incorectUsername => '请填写手机号码或者邮箱'; + + @override + String get needPassword => '请填写密码'; + @override String durationYears(int many) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/models/data_model.d.dart b/lib/models/data_model.d.dart index e17ee5c..7b0c765 100644 --- a/lib/models/data_model.d.dart +++ b/lib/models/data_model.d.dart @@ -25,4 +25,5 @@ final Map dataModelFactories = { UserGrowthInfo: UserGrowthInfo.fromJson, UserInteract: UserInteract.fromJson, UserOrg: UserOrg.fromJson, + UserPassportModel: UserPassportModel.fromJson, }; diff --git a/lib/models/data_model.g.dart b/lib/models/data_model.g.dart index b438a03..93a8a5a 100644 --- a/lib/models/data_model.g.dart +++ b/lib/models/data_model.g.dart @@ -767,3 +767,138 @@ Map _$UserOrgToJson(UserOrg instance) => { 'org_user': instance.orgUser, 'is_followed': instance.isFollowed, }; + +UserPassportModel _$UserPassportModelFromJson(Map json) => + UserPassportModel( + appId: json['app_id'] as int? ?? 0, + userId: json['user_id'] as int, + userIdStr: json['user_id_str'] as String? ?? '', + odinUserType: json['odin_user_type'] as int? ?? 0, + name: json['name'] as String, + screenName: json['screen_name'] as String? ?? '', + avatarUrl: json['avatar_url'] as String? ?? '', + userVerified: json['user_verified'] as bool? ?? false, + emailCollected: json['email_collected'] as bool? ?? false, + phoneCollected: json['phone_collected'] as bool? ?? false, + verifiedContent: json['verified_content'] as String? ?? '', + verifiedAgency: json['verified_agency'] as String? ?? '', + isBlocked: json['is_blocked'] as int? ?? 0, + isBlocking: json['is_blocking'] as int? ?? 0, + bgImgUrl: json['bg_img_url'] as String? ?? '', + gender: json['gender'] as int? ?? 0, + mediaId: json['media_id'] as int? ?? 0, + userAuthInfo: json['user_auth_info'] as String? ?? '', + industry: json['industry'] as String? ?? '', + area: json['area'] as String? ?? '', + canBeFoundByPhone: json['can_be_found_by_phone'] as int? ?? 0, + mobile: json['mobile'] as String? ?? '', + birthday: json['birthday'] as String? ?? '', + description: json['description'] as String? ?? '', + email: json['email'] as String? ?? '', + newUser: json['new_user'] as int? ?? 0, + firstLoginApp: json['first_login_app'] as int? ?? 0, + sessionKey: json['session_key'] as String, + isRecommendAllowed: json['is_recommend_allowed'] as int? ?? 0, + recommendHintMessage: json['recommend_hint_message'] as String? ?? '', + connects: json['connects'] as List?, + followingsCount: json['followings_count'] as int? ?? 0, + followersCount: json['followers_count'] as int? ?? 0, + visitCountRecent: json['visit_count_recent'] as int? ?? 0, + skipEditProfile: json['skip_edit_profile'] as int? ?? 0, + isManualSetUserInfo: json['is_manual_set_user_info'] as bool? ?? false, + deviceId: json['device_id'] as int? ?? 0, + countryCode: json['country_code'] as int? ?? 0, + hasPassword: json['has_password'] as int? ?? 0, + shareToRepost: json['share_to_repost'] as int? ?? 0, + userDecoration: json['user_decoration'] as String? ?? '', + userPrivacyExtend: json['user_privacy_extend'] as int? ?? 0, + oldUserId: json['old_user_id'] as int? ?? 0, + oldUserIdStr: json['old_user_id_str'] as String? ?? '', + secUserId: json['sec_user_id'] as String? ?? '', + secOldUserId: json['sec_old_user_id'] as String? ?? '', + vcdAccount: json['vcd_account'] as int? ?? 0, + vcdRelation: json['vcd_relation'] as int? ?? 0, + canBindVisitorAccount: json['can_bind_visitor_account'] as bool? ?? false, + isVisitorAccount: json['is_visitor_account'] as bool? ?? false, + isOnlyBindIns: json['is_only_bind_ins'] as bool? ?? false, + userDeviceRecordStatus: json['user_device_record_status'] as int? ?? 0, + isKidsMode: json['is_kids_mode'] as int? ?? 0, + isEmployee: json['is_employee'] as bool? ?? false, + passportEnterpriseUserType: + json['passport_enterprise_user_type'] as int? ?? 0, + needDeviceCreate: json['need_device_create'] as int? ?? 0, + needTtwidMigration: json['need_ttwid_migration'] as int? ?? 0, + userAuthStatus: json['user_auth_status'] as int? ?? 0, + userSafeMobile2Fa: json['user_safe_mobile2_fa'] as String? ?? '', + safeMobileCountryCode: json['safe_mobile_country_code'] as int? ?? 0, + liteUserInfoString: json['lite_user_info_string'] as String? ?? '', + liteUserInfoDemotion: json['lite_user_info_demotion'] as int? ?? 0, + appUserInfo: json['app_user_info'] as Map?, + ); + +Map _$UserPassportModelToJson(UserPassportModel instance) => + { + 'app_id': instance.appId, + 'user_id': instance.userId, + 'user_id_str': instance.userIdStr, + 'odin_user_type': instance.odinUserType, + 'name': instance.name, + 'screen_name': instance.screenName, + 'avatar_url': instance.avatarUrl, + 'user_verified': instance.userVerified, + 'email_collected': instance.emailCollected, + 'phone_collected': instance.phoneCollected, + 'verified_content': instance.verifiedContent, + 'verified_agency': instance.verifiedAgency, + 'is_blocked': instance.isBlocked, + 'is_blocking': instance.isBlocking, + 'bg_img_url': instance.bgImgUrl, + 'gender': instance.gender, + 'media_id': instance.mediaId, + 'user_auth_info': instance.userAuthInfo, + 'industry': instance.industry, + 'area': instance.area, + 'can_be_found_by_phone': instance.canBeFoundByPhone, + 'mobile': instance.mobile, + 'birthday': instance.birthday, + 'description': instance.description, + 'email': instance.email, + 'new_user': instance.newUser, + 'first_login_app': instance.firstLoginApp, + 'session_key': instance.sessionKey, + 'is_recommend_allowed': instance.isRecommendAllowed, + 'recommend_hint_message': instance.recommendHintMessage, + 'connects': instance.connects, + 'followings_count': instance.followingsCount, + 'followers_count': instance.followersCount, + 'visit_count_recent': instance.visitCountRecent, + 'skip_edit_profile': instance.skipEditProfile, + 'is_manual_set_user_info': instance.isManualSetUserInfo, + 'device_id': instance.deviceId, + 'country_code': instance.countryCode, + 'has_password': instance.hasPassword, + 'share_to_repost': instance.shareToRepost, + 'user_decoration': instance.userDecoration, + 'user_privacy_extend': instance.userPrivacyExtend, + 'old_user_id': instance.oldUserId, + 'old_user_id_str': instance.oldUserIdStr, + 'sec_user_id': instance.secUserId, + 'sec_old_user_id': instance.secOldUserId, + 'vcd_account': instance.vcdAccount, + 'vcd_relation': instance.vcdRelation, + 'can_bind_visitor_account': instance.canBindVisitorAccount, + 'is_visitor_account': instance.isVisitorAccount, + 'is_only_bind_ins': instance.isOnlyBindIns, + 'user_device_record_status': instance.userDeviceRecordStatus, + 'is_kids_mode': instance.isKidsMode, + 'is_employee': instance.isEmployee, + 'passport_enterprise_user_type': instance.passportEnterpriseUserType, + 'need_device_create': instance.needDeviceCreate, + 'need_ttwid_migration': instance.needTtwidMigration, + 'user_auth_status': instance.userAuthStatus, + 'user_safe_mobile2_fa': instance.userSafeMobile2Fa, + 'safe_mobile_country_code': instance.safeMobileCountryCode, + 'lite_user_info_string': instance.liteUserInfoString, + 'lite_user_info_demotion': instance.liteUserInfoDemotion, + 'app_user_info': instance.appUserInfo, + }; diff --git a/lib/models/repositories.dart b/lib/models/repositories.dart new file mode 100644 index 0000000..959be53 --- /dev/null +++ b/lib/models/repositories.dart @@ -0,0 +1,31 @@ +import 'package:hive/hive.dart'; + +import 'data_model.dart'; + +part 'repositories.g.dart'; + +@HiveType(typeId: 1, adapterName: 'UserAuthenAdapter') +class UserAuthen extends HiveObject { + UserAuthen({ + this.token = '', + this.refreshToken = '', + this.expireIn = 0, + this.timestamp = 0, + }); + + @HiveField(0) + String token; + @HiveField(1) + String refreshToken; + @HiveField(2) + int expireIn; + @HiveField(3) + int timestamp; + + Json toJson() => { + 'token': token, + 'refreshToken': refreshToken, + 'expireIn': expireIn, + 'timestamp': timestamp, + }; +} diff --git a/lib/models/repositories.g.dart b/lib/models/repositories.g.dart new file mode 100644 index 0000000..92b0052 --- /dev/null +++ b/lib/models/repositories.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'repositories.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class UserAuthenAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + UserAuthen read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return UserAuthen( + token: fields[0] as String, + refreshToken: fields[1] as String, + expireIn: fields[2] as int, + timestamp: fields[3] as int, + ); + } + + @override + void write(BinaryWriter writer, UserAuthen obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.token) + ..writeByte(1) + ..write(obj.refreshToken) + ..writeByte(2) + ..write(obj.expireIn) + ..writeByte(3) + ..write(obj.timestamp); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserAuthenAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart index a41dc01..1679a37 100644 --- a/lib/models/user_model.dart +++ b/lib/models/user_model.dart @@ -322,3 +322,219 @@ class UserOrg extends DataModel { @override List get props => [orgInfo, orgUser, isFollowed]; } + +@JsonSerializable() +class UserPassportModel extends DataModel { + const UserPassportModel({ + this.appId = 0, + required this.userId, + this.userIdStr = '', + this.odinUserType = 0, + required this.name, + this.screenName = '', + this.avatarUrl = '', + this.userVerified = false, + this.emailCollected = false, + this.phoneCollected = false, + this.verifiedContent = '', + this.verifiedAgency = '', + this.isBlocked = 0, + this.isBlocking = 0, + this.bgImgUrl = '', + this.gender = 0, + this.mediaId = 0, + this.userAuthInfo = '', + this.industry = '', + this.area = '', + this.canBeFoundByPhone = 0, + this.mobile = '', + this.birthday = '', + this.description = '', + this.email = '', + this.newUser = 0, + this.firstLoginApp = 0, + required this.sessionKey, + this.isRecommendAllowed = 0, + this.recommendHintMessage = '', + this.connects, + this.followingsCount = 0, + this.followersCount = 0, + this.visitCountRecent = 0, + this.skipEditProfile = 0, + this.isManualSetUserInfo = false, + this.deviceId = 0, + this.countryCode = 0, + this.hasPassword = 0, + this.shareToRepost = 0, + this.userDecoration = '', + this.userPrivacyExtend = 0, + this.oldUserId = 0, + this.oldUserIdStr = '', + this.secUserId = '', + this.secOldUserId = '', + this.vcdAccount = 0, + this.vcdRelation = 0, + this.canBindVisitorAccount = false, + this.isVisitorAccount = false, + this.isOnlyBindIns = false, + this.userDeviceRecordStatus = 0, + this.isKidsMode = 0, + this.isEmployee = false, + this.passportEnterpriseUserType = 0, + this.needDeviceCreate = 0, + this.needTtwidMigration = 0, + this.userAuthStatus = 0, + this.userSafeMobile2Fa = '', + this.safeMobileCountryCode = 0, + this.liteUserInfoString = '', + this.liteUserInfoDemotion = 0, + this.appUserInfo, + }); + + const UserPassportModel.empty() + : this( + userId: 0, + name: '', + sessionKey: '', + ); + + factory UserPassportModel.fromJson(Map json) => + _$UserPassportModelFromJson(json); + + bool get isEmpty => userId == 0; + + bool get isLogin => !isEmpty && sessionKey.isNotEmpty; + + final int appId; + final int userId; + final String userIdStr; + final int odinUserType; + final String name; + final String screenName; + final String avatarUrl; + final bool userVerified; + final bool emailCollected; + final bool phoneCollected; + final String verifiedContent; + final String verifiedAgency; + final int isBlocked; + final int isBlocking; + final String bgImgUrl; + final int gender; + final int mediaId; + final String userAuthInfo; + final String industry; + final String area; + final int canBeFoundByPhone; + final String mobile; + final String birthday; + final String description; + final String email; + final int newUser; + final int firstLoginApp; + final String sessionKey; + final int isRecommendAllowed; + final String recommendHintMessage; + final List? connects; + final int followingsCount; + final int followersCount; + final int visitCountRecent; + final int skipEditProfile; + final bool isManualSetUserInfo; + final int deviceId; + final int countryCode; + final int hasPassword; + final int shareToRepost; + final String userDecoration; + final int userPrivacyExtend; + final int oldUserId; + final String oldUserIdStr; + final String secUserId; + final String secOldUserId; + final int vcdAccount; + final int vcdRelation; + final bool canBindVisitorAccount; + final bool isVisitorAccount; + final bool isOnlyBindIns; + final int userDeviceRecordStatus; + final int isKidsMode; + final bool isEmployee; + final int passportEnterpriseUserType; + final int needDeviceCreate; + final int needTtwidMigration; + final int userAuthStatus; + final String userSafeMobile2Fa; + final int safeMobileCountryCode; + final String liteUserInfoString; + final int liteUserInfoDemotion; + final Json? appUserInfo; + + @override + List get props => [ + appId, + userId, + userIdStr, + odinUserType, + name, + screenName, + avatarUrl, + userVerified, + emailCollected, + phoneCollected, + verifiedContent, + verifiedAgency, + isBlocked, + isBlocking, + bgImgUrl, + gender, + mediaId, + userAuthInfo, + industry, + area, + canBeFoundByPhone, + mobile, + birthday, + description, + email, + newUser, + sessionKey, + isRecommendAllowed, + recommendHintMessage, + connects, + followingsCount, + followersCount, + visitCountRecent, + skipEditProfile, + isManualSetUserInfo, + deviceId, + countryCode, + hasPassword, + shareToRepost, + userDecoration, + userPrivacyExtend, + oldUserId, + oldUserIdStr, + secUserId, + secOldUserId, + vcdAccount, + vcdRelation, + canBindVisitorAccount, + isVisitorAccount, + isOnlyBindIns, + userDeviceRecordStatus, + isKidsMode, + isEmployee, + passportEnterpriseUserType, + needDeviceCreate, + needTtwidMigration, + userAuthStatus, + userSafeMobile2Fa, + safeMobileCountryCode, + liteUserInfoString, + liteUserInfoDemotion, + appUserInfo, + ]; + + @override + Json toJson() => _$UserPassportModelToJson(this); +} diff --git a/lib/pages/home/mine.dart b/lib/pages/home/mine.dart index b1610b9..3018da1 100644 --- a/lib/pages/home/mine.dart +++ b/lib/pages/home/mine.dart @@ -3,6 +3,7 @@ // LICENSE file. import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:juejin/exports.dart'; class MinePage extends StatefulWidget { @@ -37,36 +38,48 @@ class _MinePageState extends State { ); } - Widget _buildUserAvatar(BuildContext context) { - return Container( - padding: const EdgeInsets.all(8), - color: context.theme.dividerColor, - child: SvgPicture.asset(R.ASSETS_BRAND_SVG), - ); - } - - Widget _buildUsername(BuildContext context) { - return Text( - context.l10n.userSignInOrUp, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - ); - } - Widget _buildUser(BuildContext context) { - return SizedBox( - height: 54, - child: Row( - children: [ - AspectRatio( - aspectRatio: 1, - child: ClipOval( - child: _buildUserAvatar(context), + return Consumer( + builder: (BuildContext context, WidgetRef ref, Widget? child) { + final userModel = ref.watch(userProvider); + return GestureDetector( + onTap: () => context.navigator.pushNamed( + userModel.isLogin ? Routes.userProfile.name : Routes.loginPage.name, + ), + child: SizedBox( + height: 54, + child: Row( + children: [ + AspectRatio( + aspectRatio: 1, + child: ClipOval( + child: userModel.isLogin + ? Image.network( + userModel.avatarUrl, + fit: BoxFit.cover, + ) + : Container( + padding: const EdgeInsets.all(8), + color: context.theme.dividerColor, + child: SvgPicture.asset(R.ASSETS_BRAND_SVG), + ), + ), + ), + const Gap.h(16), + Text( + userModel.isLogin + ? userModel.screenName + : context.l10n.userSignInOrUp, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], ), ), - const Gap.h(16), - _buildUsername(context), - ], - ), + ); + }, ); } diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart new file mode 100644 index 0000000..7a12900 --- /dev/null +++ b/lib/pages/login/login.dart @@ -0,0 +1,149 @@ +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../exports.dart'; + +@FFRoute(name: 'login-page') +class LoginPage extends ConsumerStatefulWidget { + const LoginPage({Key? key}) : super(key: key); + + @override + ConsumerState createState() => _LoginPageState(); +} + +class _LoginPageState extends ConsumerState { + final GlobalKey _formKey = GlobalKey(); + + String? _username; + String? _password; + + @override + void initState() { + super.initState(); + } + + Future _doLogin() async { + if (_formKey.currentState!.validate()) { + try { + final result = await PassportAPI.login(_username!, _password!); + if (context.mounted) { + if (result.isSucceed) { + showToast(context.l10n.loginSuccess); + ref.read(userProvider.notifier).update(result.data!); + context.navigator.pop(); + } else { + showToast(result.msg); + } + } + } on ModelMakeError catch (e) { + showToast(e.json['description'] ?? '${e.json}'); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + systemOverlayStyle: SystemUiOverlayStyle.dark, + ), + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 32.0), + child: Text( + context.l10n.loginSlogan, + style: context.textTheme.titleLarge, + ), + ), + const Gap.v(16), + TextFormField( + validator: (v) { + if (v == null || v.isEmpty) { + return context.l10n.needUsername; + } + if (!v.isMobile() && !v.isEmail()) { + return context.l10n.incorectUsername; + } + return null; + }, + onChanged: (v) => _username = v, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + hintText: context.l10n.hintUsername, + prefixIcon: const Icon(Icons.person_outline), + focusColor: Colors.red, + prefixIconColor: Colors.red, + ), + ), + const Gap.v(8), + TextFormField( + obscureText: true, + obscuringCharacter: '*', + validator: (v) { + if (v?.isEmpty ?? true) { + return context.l10n.needPassword; + } + return null; + }, + onChanged: (v) => _password = v, + keyboardType: TextInputType.visiblePassword, + decoration: InputDecoration( + hintText: context.l10n.hintPassword, + prefixIcon: const Icon(Icons.lock_outline), + ), + ), + const Gap.v(16), + ElevatedButton( + onPressed: _doLogin, + child: Center( + child: Text(context.l10n.buttonSignIn), + ), + ), + const Gap.v(8), + ], + ), + ), + ), + bottomNavigationBar: SafeArea( + child: AnimatedPadding( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutQuart, + padding: EdgeInsets.only( + left: 16.0, + right: 16, + bottom: math.max(16, context.bottomViewInsets), + ), + child: Row( + children: [ + TextButton( + onPressed: () { + showToast(context.l10n.notSupported); + }, + child: Text(context.l10n.linkSignUp), + ), + const Spacer(), + TextButton( + onPressed: () { + showToast(context.l10n.notSupported); + }, + child: Text(context.l10n.linkRetrieve), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/splash.dart b/lib/pages/splash.dart index 53873da..efe9e80 100644 --- a/lib/pages/splash.dart +++ b/lib/pages/splash.dart @@ -4,17 +4,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:juejin/exports.dart'; @FFRoute(name: 'splash-page') -class SplashPage extends StatefulWidget { +class SplashPage extends ConsumerStatefulWidget { const SplashPage({super.key}); @override - State createState() => _SplashPageState(); + ConsumerState createState() => _SplashPageState(); } -class _SplashPageState extends State { +class _SplashPageState extends ConsumerState { @override void initState() { super.initState(); @@ -36,9 +37,25 @@ class _SplashPageState extends State { DeviceUtil.initDeviceInfo(forceRefresh: true), PackageUtil.initInfo(), HttpUtil.init(), + HiveUtil.init(), ]); await DeviceUtil.setHighestRefreshRate(); await HttpUtil.fetch(FetchType.get, url: 'https://${Urls.domain}'); + + final tokenNotifier = ref.read(tokenProvider.notifier); + tokenNotifier.restore(); + + if (tokenNotifier.isLogin) { + HttpUtil.setHeaders('token', tokenNotifier.token); + PassportAPI.restore().then((data) { + if (data.isSucceed) { + ref.read(userProvider.notifier).update(data.data!); + } else { + tokenNotifier.logout(); + } + }); + } + if (mounted) { navigator.pushReplacementNamed(Routes.homePage.name); } diff --git a/lib/pages/user/profile.dart b/lib/pages/user/profile.dart new file mode 100644 index 0000000..560543a --- /dev/null +++ b/lib/pages/user/profile.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import '../../exports.dart'; + +@FFRoute(name: 'user-profile') +class ProfilePage extends StatefulWidget { + const ProfilePage({super.key}); + + @override + State createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Center( + child: Text(context.l10n.notSupported), + ), + ); + } +} diff --git a/lib/routes/juejin_route.dart b/lib/routes/juejin_route.dart index f1baf1d..f4027fc 100644 --- a/lib/routes/juejin_route.dart +++ b/lib/routes/juejin_route.dart @@ -2,7 +2,10 @@ // ************************************************************************** // Auto generated by https://github.com/fluttercandies/ff_annotation_route // ************************************************************************** -// ignore_for_file: prefer_const_literals_to_create_immutables,unused_local_variable,unused_import,unnecessary_import +// fast mode: true +// version: 10.0.9 +// ************************************************************************** +// ignore_for_file: prefer_const_literals_to_create_immutables,unused_local_variable,unused_import,unnecessary_import,unused_shown_name,implementation_imports,duplicate_import import 'package:ff_annotation_route_library/ff_annotation_route_library.dart'; import 'package:flutter/widgets.dart'; import 'package:juejin/exports.dart'; @@ -10,8 +13,11 @@ import 'package:juejin/exports.dart'; import '../pages/article/detail.dart'; import '../pages/club/club.dart'; import '../pages/home.dart'; +import '../pages/login/login.dart'; import '../pages/splash.dart'; +import '../pages/user/profile.dart'; +/// Get route settings base on route name, auto generated by https://github.com/fluttercandies/ff_annotation_route FFRouteSettings getRouteSettings({ required String name, Map? arguments, @@ -62,6 +68,16 @@ FFRouteSettings getRouteSettings({ ), ), ); + case 'login-page': + return FFRouteSettings( + name: name, + arguments: arguments, + builder: () => LoginPage( + key: asT( + safeArguments['key'], + ), + ), + ); case 'splash-page': return FFRouteSettings( name: name, @@ -72,6 +88,16 @@ FFRouteSettings getRouteSettings({ ), ), ); + case 'user-profile': + return FFRouteSettings( + name: name, + arguments: arguments, + builder: () => ProfilePage( + key: asT( + safeArguments['key'], + ), + ), + ); default: return FFRouteSettings( name: FFRoute.notFoundName, diff --git a/lib/routes/juejin_routes.dart b/lib/routes/juejin_routes.dart index e1adef3..a546455 100644 --- a/lib/routes/juejin_routes.dart +++ b/lib/routes/juejin_routes.dart @@ -2,17 +2,24 @@ // ************************************************************************** // Auto generated by https://github.com/fluttercandies/ff_annotation_route // ************************************************************************** -// ignore_for_file: prefer_const_literals_to_create_immutables,unused_local_variable,unused_import,unnecessary_import +// fast mode: true +// version: 10.0.9 +// ************************************************************************** +// ignore_for_file: prefer_const_literals_to_create_immutables,unused_local_variable,unused_import,unnecessary_import,unused_shown_name,implementation_imports,duplicate_import import 'package:flutter/foundation.dart'; import 'package:juejin/exports.dart'; +/// The routeNames auto generated by https://github.com/fluttercandies/ff_annotation_route const List routeNames = [ 'article-detail-page', 'club-page', 'home-page', + 'login-page', 'splash-page', + 'user-profile', ]; +/// The routes auto generated by https://github.com/fluttercandies/ff_annotation_route class Routes { const Routes._(); @@ -43,6 +50,15 @@ class Routes { /// HomePage : [Key? key] static const _HomePage homePage = _HomePage(); + /// 'login-page' + /// + /// [name] : 'login-page' + /// + /// [constructors] : + /// + /// LoginPage : [Key? key] + static const _LoginPage loginPage = _LoginPage(); + /// 'splash-page' /// /// [name] : 'splash-page' @@ -51,6 +67,11 @@ class Routes { /// /// SplashPage : [Key? key] static const _SplashPage splashPage = _SplashPage(); + + /// 'user-profile' + /// + /// [name] : 'user-profile' + static const _UserProfile userProfile = _UserProfile(); } class _ArticleDetailPage { @@ -109,6 +130,22 @@ class _HomePage { String toString() => name; } +class _LoginPage { + const _LoginPage(); + + String get name => 'login-page'; + + Map d({ + Key? key, + }) => + { + 'key': key, + }; + + @override + String toString() => name; +} + class _SplashPage { const _SplashPage(); @@ -124,3 +161,19 @@ class _SplashPage { @override String toString() => name; } + +class _UserProfile { + const _UserProfile(); + + String get name => 'user-profile'; + + Map d({ + Key? key, + }) => + { + 'key': key, + }; + + @override + String toString() => name; +} diff --git a/lib/utils/hive_util.dart b/lib/utils/hive_util.dart new file mode 100644 index 0000000..2d014e6 --- /dev/null +++ b/lib/utils/hive_util.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../models/repositories.dart'; + +class HiveUtil { + HiveUtil._(); + + static const userToken = 'user_token'; + + static Box? _box; + static Box get box => _box!; + + static Future init() async { + const secureStorage = FlutterSecureStorage(); + String? secureKey = await secureStorage.read(key: 'key'); + if (secureKey == null) { + final key = Hive.generateSecureKey(); + await secureStorage.write( + key: 'key', + value: base64UrlEncode(key), + ); + secureKey = (await secureStorage.read(key: 'key'))!; + } + + final encryptionKey = base64Url.decode(secureKey); + + final appDocDir = await getApplicationDocumentsDirectory(); + + await initByKey(encryptionKey, appDocDir.path); + } + + @visibleForTesting + static Future initByKey(List key, String path) async { + Hive.registerAdapter(UserAuthenAdapter()); + + _box = await Hive.openBox( + HiveUtil.userToken, + encryptionCipher: HiveAesCipher(key), + path: '${path.replaceFirst(RegExp(r'[\\/]$'), '')}/', + ); + } + + /// open another box + static Future> openBox(String name) { + assert(name != HiveUtil.userToken, 'Please use HiveUtil.box'); + return Hive.openBox( + name, + path: box.path, + ); + } + + static String get name => box.name; + + static void close() { + box.close(); + } + + static Future deleteFromDisk() { + return box.deleteFromDisk(); + } +} diff --git a/lib/utils/http_util.dart b/lib/utils/http_util.dart index 7a71d29..3afb17e 100644 --- a/lib/utils/http_util.dart +++ b/lib/utils/http_util.dart @@ -49,11 +49,20 @@ class HttpUtil { Directory(cookiesPath).createSync(recursive: true); cookieJar = PersistCookieJar(storage: FileStorage(cookiesPath)); cookieManager = CookieManager(cookieJar); + dio = Dio() ..options = baseOptions ..interceptors.addAll([cookieManager, _interceptor]); } + static void setHeaders(String key, String value) { + dio.options.headers.addAll({key: value}); + } + + static void removeHeaders(String key) { + dio.options.headers.remove(key); + } + static ResponseModel _successModel() => ResponseModel.succeed(); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f6f23bf..38dd0bc 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f16b4c3..65240e9 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2876d16..d9759ce 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,12 +6,14 @@ import FlutterMacOS import Foundation import device_info_plus +import flutter_secure_storage_macos import package_info_plus import path_provider_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.yaml b/pubspec.yaml index 143b638..7a55cbc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,10 @@ dependencies: flutter_inappwebview: ^5.4.3+7 flutter_localizations: sdk: flutter + flutter_riverpod: ^2.0.2 + flutter_secure_storage: ^6.0.0 flutter_svg: ^2.0.7 + hive: ^2.2.3 intl: any json_annotation: ^4.6.0 loading_more_list: ^5.0.0 @@ -32,6 +35,7 @@ dependencies: package_info_plus: ^4.2.0 path_provider: ^2.0.11 pull_to_refresh_notification: ^3.1.0 + riverpod: ^2.0.2 url_launcher: ^6.1.5 visibility_detector: ^0.4.0+2 @@ -43,6 +47,7 @@ dev_dependencies: flutter_lints: ^3.0.0 flutter_test: sdk: flutter + hive_generator: ^1.1.3 json_serializable: ^6.3.1 path: ^1.8.1 process_run: ^0.13.2 diff --git a/test/api_test.dart b/test/api_test.dart index 7ffd34a..b230c25 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -27,4 +27,22 @@ void main() { final cates = await RecommendAPI.getAllFeedArticles(); assert(cates.isSucceed); }); + + test('PassportAPI', () async { + // {data: {captcha: , desc_url: , description: 帐号或密码错误,剩余2次机会, error_code: 1033}, message: error} + // {data: {captcha: , desc_url: , description: 帐号或密码错误, error_code: 1009}, message: error} + // {data: UserPassportModel, message: success} + + expect( + () => PassportAPI.login('test@test.com', 'errorpassword'), + throwsA(const TypeMatcher>()), + ); + + final passport = await PassportAPI.login( + 'username', + 'password', + ); + + assert(passport.isSucceed); + }); } diff --git a/test/hive_test.dart b/test/hive_test.dart new file mode 100644 index 0000000..5a07bae --- /dev/null +++ b/test/hive_test.dart @@ -0,0 +1,31 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:juejin/exports.dart'; + +const hiveKey = [ + 69, 65, 214, 189, 167, 95, 128, 69, // + 125, 194, 153, 172, 57, 105, 57, 253, + 20, 174, 135, 53, 77, 167, 78, 244, + 248, 168, 30, 29, 185, 35, 89, 139, +]; + +final hivePath = '${Directory.current.path}/test/hive'; + +void main() { + test('user_token', () async { + await HiveUtil.initByKey(hiveKey, hivePath); + + final box = HiveUtil.box; + + expect(box.name, HiveUtil.userToken); + + await box.clear(); + await box.put('token', UserAuthen(token: 'test-token')); + + final token = box.get('token'); + expect(token?.token, 'test-token'); + + HiveUtil.close(); + }); +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4f78848..2048c45 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 88b22e5..de626cc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows url_launcher_windows )