✨ Brand new sign in flow
This commit is contained in:
		@@ -3,6 +3,7 @@
 | 
				
			|||||||
  "hide": "Hide",
 | 
					  "hide": "Hide",
 | 
				
			||||||
  "okay": "Okay",
 | 
					  "okay": "Okay",
 | 
				
			||||||
  "next": "Next",
 | 
					  "next": "Next",
 | 
				
			||||||
 | 
					  "prev": "Previous",
 | 
				
			||||||
  "reset": "Reset",
 | 
					  "reset": "Reset",
 | 
				
			||||||
  "page": "Page",
 | 
					  "page": "Page",
 | 
				
			||||||
  "home": "Home",
 | 
					  "home": "Home",
 | 
				
			||||||
@@ -70,8 +71,12 @@
 | 
				
			|||||||
  "forgotPassword": "Forgot password",
 | 
					  "forgotPassword": "Forgot password",
 | 
				
			||||||
  "email": "Email",
 | 
					  "email": "Email",
 | 
				
			||||||
  "username": "Username",
 | 
					  "username": "Username",
 | 
				
			||||||
 | 
					  "usernameInputHint": "Also supports email and phone number",
 | 
				
			||||||
  "nickname": "Nickname",
 | 
					  "nickname": "Nickname",
 | 
				
			||||||
  "password": "Password",
 | 
					  "password": "Password",
 | 
				
			||||||
 | 
					  "passwordOneTime": "One-time-password",
 | 
				
			||||||
 | 
					  "passwordInputHint": "Forgot your password? Go back to the first step to reset your password",
 | 
				
			||||||
 | 
					  "passwordOneTimeInputHint": "Check your inbox or authorizer for a verification code",
 | 
				
			||||||
  "title": "Title",
 | 
					  "title": "Title",
 | 
				
			||||||
  "description": "Description",
 | 
					  "description": "Description",
 | 
				
			||||||
  "birthday": "Birthday",
 | 
					  "birthday": "Birthday",
 | 
				
			||||||
@@ -103,6 +108,11 @@
 | 
				
			|||||||
  "signinRiskDetected": "Risk detected, click Next to open a webpage and signin through it to pass security check.",
 | 
					  "signinRiskDetected": "Risk detected, click Next to open a webpage and signin through it to pass security check.",
 | 
				
			||||||
  "signinResetPasswordHint": "Please enter username to request reset password.",
 | 
					  "signinResetPasswordHint": "Please enter username to request reset password.",
 | 
				
			||||||
  "signinResetPasswordSent": "Reset password request sent, check your inbox!",
 | 
					  "signinResetPasswordSent": "Reset password request sent, check your inbox!",
 | 
				
			||||||
 | 
					  "signinPickFactor": "Pick a way\nfor verification",
 | 
				
			||||||
 | 
					  "signinEnterPassword": "Enter your\npassword",
 | 
				
			||||||
 | 
					  "signinMultiFactor": "@n step(s) verifications",
 | 
				
			||||||
 | 
					  "authFactorEmail": "Email One-time-password",
 | 
				
			||||||
 | 
					  "authFactorPassword": "Password",
 | 
				
			||||||
  "signup": "Sign up",
 | 
					  "signup": "Sign up",
 | 
				
			||||||
  "signupGreeting": "Welcome onboard",
 | 
					  "signupGreeting": "Welcome onboard",
 | 
				
			||||||
  "signupCaption": "Create an account on Solarpass and then get the access of entire Solar Network!",
 | 
					  "signupCaption": "Create an account on Solarpass and then get the access of entire Solar Network!",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@
 | 
				
			|||||||
  "okay": "确认",
 | 
					  "okay": "确认",
 | 
				
			||||||
  "home": "首页",
 | 
					  "home": "首页",
 | 
				
			||||||
  "next": "下一步",
 | 
					  "next": "下一步",
 | 
				
			||||||
 | 
					  "prev": "上一步",
 | 
				
			||||||
  "reset": "重置",
 | 
					  "reset": "重置",
 | 
				
			||||||
  "cancel": "取消",
 | 
					  "cancel": "取消",
 | 
				
			||||||
  "confirm": "确认",
 | 
					  "confirm": "确认",
 | 
				
			||||||
@@ -75,8 +76,12 @@
 | 
				
			|||||||
  "forgotPassword": "忘记密码",
 | 
					  "forgotPassword": "忘记密码",
 | 
				
			||||||
  "email": "邮件地址",
 | 
					  "email": "邮件地址",
 | 
				
			||||||
  "username": "用户名",
 | 
					  "username": "用户名",
 | 
				
			||||||
 | 
					  "usernameInputHint": "同时支持邮箱 / 电话号码",
 | 
				
			||||||
  "nickname": "显示名",
 | 
					  "nickname": "显示名",
 | 
				
			||||||
  "password": "密码",
 | 
					  "password": "密码",
 | 
				
			||||||
 | 
					  "passwordOneTime": "一次性验证码",
 | 
				
			||||||
 | 
					  "passwordInputHint": "忘记密码了?回到第一步以重置密码",
 | 
				
			||||||
 | 
					  "passwordOneTimeInputHint": "检查你的收件箱或是授权器获得以验证码",
 | 
				
			||||||
  "title": "标题",
 | 
					  "title": "标题",
 | 
				
			||||||
  "description": "简介",
 | 
					  "description": "简介",
 | 
				
			||||||
  "birthday": "生日",
 | 
					  "birthday": "生日",
 | 
				
			||||||
@@ -108,6 +113,11 @@
 | 
				
			|||||||
  "signinRiskDetected": "检测到风险,点击下一步按钮来打开一个网页,并通过在其上面登录来通过安全检查。",
 | 
					  "signinRiskDetected": "检测到风险,点击下一步按钮来打开一个网页,并通过在其上面登录来通过安全检查。",
 | 
				
			||||||
  "signinResetPasswordHint": "请先填写用户名以发送重置密码请求。",
 | 
					  "signinResetPasswordHint": "请先填写用户名以发送重置密码请求。",
 | 
				
			||||||
  "signinResetPasswordSent": "重置密码请求已发送,在绑定邮件收件箱可收取一份包含重置密码链接的邮件。",
 | 
					  "signinResetPasswordSent": "重置密码请求已发送,在绑定邮件收件箱可收取一份包含重置密码链接的邮件。",
 | 
				
			||||||
 | 
					  "signinPickFactor": "选择一个\n验证方式",
 | 
				
			||||||
 | 
					  "signinEnterPassword": "输入密码\n或验证码",
 | 
				
			||||||
 | 
					  "signinMultiFactor": "@n 步验证",
 | 
				
			||||||
 | 
					  "authFactorEmail": "邮箱一次性密码",
 | 
				
			||||||
 | 
					  "authFactorPassword": "账户密码",
 | 
				
			||||||
  "signup": "注册",
 | 
					  "signup": "注册",
 | 
				
			||||||
  "signupGreeting": "欢迎加入\nSolar Network",
 | 
					  "signupGreeting": "欢迎加入\nSolar Network",
 | 
				
			||||||
  "signupCaption": "在 Solarpass 注册一个账号以获得整个 Solar Network 的存取权!",
 | 
					  "signupCaption": "在 Solarpass 注册一个账号以获得整个 Solar Network 的存取权!",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										103
									
								
								lib/models/auth.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								lib/models/auth.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
				
			|||||||
 | 
					import 'package:freezed_annotation/freezed_annotation.dart';
 | 
				
			||||||
 | 
					import 'package:solian/models/account.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part 'auth.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					class AuthResult {
 | 
				
			||||||
 | 
					  bool isFinished;
 | 
				
			||||||
 | 
					  AuthTicket ticket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  AuthResult({
 | 
				
			||||||
 | 
					    required this.isFinished,
 | 
				
			||||||
 | 
					    required this.ticket,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory AuthResult.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					      _$AuthResultFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() => _$AuthResultToJson(this);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					class AuthTicket {
 | 
				
			||||||
 | 
					  int id;
 | 
				
			||||||
 | 
					  DateTime createdAt;
 | 
				
			||||||
 | 
					  DateTime updatedAt;
 | 
				
			||||||
 | 
					  DateTime? deletedAt;
 | 
				
			||||||
 | 
					  String location;
 | 
				
			||||||
 | 
					  String ipAddress;
 | 
				
			||||||
 | 
					  String userAgent;
 | 
				
			||||||
 | 
					  int stepRemain;
 | 
				
			||||||
 | 
					  List<String> claims;
 | 
				
			||||||
 | 
					  List<String> audiences;
 | 
				
			||||||
 | 
					  @JsonKey(defaultValue: [])
 | 
				
			||||||
 | 
					  List<int> factorTrail;
 | 
				
			||||||
 | 
					  String? grantToken;
 | 
				
			||||||
 | 
					  String? accessToken;
 | 
				
			||||||
 | 
					  String? refreshToken;
 | 
				
			||||||
 | 
					  DateTime? expiredAt;
 | 
				
			||||||
 | 
					  DateTime? availableAt;
 | 
				
			||||||
 | 
					  DateTime? lastGrantAt;
 | 
				
			||||||
 | 
					  String? nonce;
 | 
				
			||||||
 | 
					  int? clientId;
 | 
				
			||||||
 | 
					  Account account;
 | 
				
			||||||
 | 
					  int accountId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  AuthTicket({
 | 
				
			||||||
 | 
					    required this.id,
 | 
				
			||||||
 | 
					    required this.createdAt,
 | 
				
			||||||
 | 
					    required this.updatedAt,
 | 
				
			||||||
 | 
					    required this.deletedAt,
 | 
				
			||||||
 | 
					    required this.location,
 | 
				
			||||||
 | 
					    required this.ipAddress,
 | 
				
			||||||
 | 
					    required this.userAgent,
 | 
				
			||||||
 | 
					    required this.stepRemain,
 | 
				
			||||||
 | 
					    required this.claims,
 | 
				
			||||||
 | 
					    required this.audiences,
 | 
				
			||||||
 | 
					    required this.factorTrail,
 | 
				
			||||||
 | 
					    required this.grantToken,
 | 
				
			||||||
 | 
					    required this.accessToken,
 | 
				
			||||||
 | 
					    required this.refreshToken,
 | 
				
			||||||
 | 
					    required this.expiredAt,
 | 
				
			||||||
 | 
					    required this.availableAt,
 | 
				
			||||||
 | 
					    required this.lastGrantAt,
 | 
				
			||||||
 | 
					    required this.nonce,
 | 
				
			||||||
 | 
					    required this.clientId,
 | 
				
			||||||
 | 
					    required this.account,
 | 
				
			||||||
 | 
					    required this.accountId,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory AuthTicket.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					      _$AuthTicketFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() => _$AuthTicketToJson(this);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					class AuthFactor {
 | 
				
			||||||
 | 
					  int id;
 | 
				
			||||||
 | 
					  DateTime createdAt;
 | 
				
			||||||
 | 
					  DateTime updatedAt;
 | 
				
			||||||
 | 
					  DateTime? deletedAt;
 | 
				
			||||||
 | 
					  int type;
 | 
				
			||||||
 | 
					  Map<String, dynamic>? config;
 | 
				
			||||||
 | 
					  Account account;
 | 
				
			||||||
 | 
					  int accountId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  AuthFactor({
 | 
				
			||||||
 | 
					    required this.id,
 | 
				
			||||||
 | 
					    required this.createdAt,
 | 
				
			||||||
 | 
					    required this.updatedAt,
 | 
				
			||||||
 | 
					    required this.deletedAt,
 | 
				
			||||||
 | 
					    required this.type,
 | 
				
			||||||
 | 
					    required this.config,
 | 
				
			||||||
 | 
					    required this.account,
 | 
				
			||||||
 | 
					    required this.accountId,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory AuthFactor.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					      _$AuthFactorFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() => _$AuthFactorToJson(this);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										105
									
								
								lib/models/auth.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								lib/models/auth.g.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
				
			|||||||
 | 
					// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of 'auth.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					// JsonSerializableGenerator
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					AuthResult _$AuthResultFromJson(Map<String, dynamic> json) => AuthResult(
 | 
				
			||||||
 | 
					      isFinished: json['is_finished'] as bool,
 | 
				
			||||||
 | 
					      ticket: AuthTicket.fromJson(json['ticket'] as Map<String, dynamic>),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Map<String, dynamic> _$AuthResultToJson(AuthResult instance) =>
 | 
				
			||||||
 | 
					    <String, dynamic>{
 | 
				
			||||||
 | 
					      'is_finished': instance.isFinished,
 | 
				
			||||||
 | 
					      'ticket': instance.ticket.toJson(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					AuthTicket _$AuthTicketFromJson(Map<String, dynamic> json) => AuthTicket(
 | 
				
			||||||
 | 
					      id: (json['id'] as num).toInt(),
 | 
				
			||||||
 | 
					      createdAt: DateTime.parse(json['created_at'] as String),
 | 
				
			||||||
 | 
					      updatedAt: DateTime.parse(json['updated_at'] as String),
 | 
				
			||||||
 | 
					      deletedAt: json['deleted_at'] == null
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : DateTime.parse(json['deleted_at'] as String),
 | 
				
			||||||
 | 
					      location: json['location'] as String,
 | 
				
			||||||
 | 
					      ipAddress: json['ip_address'] as String,
 | 
				
			||||||
 | 
					      userAgent: json['user_agent'] as String,
 | 
				
			||||||
 | 
					      stepRemain: (json['step_remain'] as num).toInt(),
 | 
				
			||||||
 | 
					      claims:
 | 
				
			||||||
 | 
					          (json['claims'] as List<dynamic>).map((e) => e as String).toList(),
 | 
				
			||||||
 | 
					      audiences:
 | 
				
			||||||
 | 
					          (json['audiences'] as List<dynamic>).map((e) => e as String).toList(),
 | 
				
			||||||
 | 
					      factorTrail: (json['factor_trail'] as List<dynamic>?)
 | 
				
			||||||
 | 
					              ?.map((e) => (e as num).toInt())
 | 
				
			||||||
 | 
					              .toList() ??
 | 
				
			||||||
 | 
					          [],
 | 
				
			||||||
 | 
					      grantToken: json['grant_token'] as String?,
 | 
				
			||||||
 | 
					      accessToken: json['access_token'] as String?,
 | 
				
			||||||
 | 
					      refreshToken: json['refresh_token'] as String?,
 | 
				
			||||||
 | 
					      expiredAt: json['expired_at'] == null
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : DateTime.parse(json['expired_at'] as String),
 | 
				
			||||||
 | 
					      availableAt: json['available_at'] == null
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : DateTime.parse(json['available_at'] as String),
 | 
				
			||||||
 | 
					      lastGrantAt: json['last_grant_at'] == null
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : DateTime.parse(json['last_grant_at'] as String),
 | 
				
			||||||
 | 
					      nonce: json['nonce'] as String?,
 | 
				
			||||||
 | 
					      clientId: (json['client_id'] as num?)?.toInt(),
 | 
				
			||||||
 | 
					      account: Account.fromJson(json['account'] as Map<String, dynamic>),
 | 
				
			||||||
 | 
					      accountId: (json['account_id'] as num).toInt(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Map<String, dynamic> _$AuthTicketToJson(AuthTicket instance) =>
 | 
				
			||||||
 | 
					    <String, dynamic>{
 | 
				
			||||||
 | 
					      'id': instance.id,
 | 
				
			||||||
 | 
					      'created_at': instance.createdAt.toIso8601String(),
 | 
				
			||||||
 | 
					      'updated_at': instance.updatedAt.toIso8601String(),
 | 
				
			||||||
 | 
					      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
				
			||||||
 | 
					      'location': instance.location,
 | 
				
			||||||
 | 
					      'ip_address': instance.ipAddress,
 | 
				
			||||||
 | 
					      'user_agent': instance.userAgent,
 | 
				
			||||||
 | 
					      'step_remain': instance.stepRemain,
 | 
				
			||||||
 | 
					      'claims': instance.claims,
 | 
				
			||||||
 | 
					      'audiences': instance.audiences,
 | 
				
			||||||
 | 
					      'factor_trail': instance.factorTrail,
 | 
				
			||||||
 | 
					      'grant_token': instance.grantToken,
 | 
				
			||||||
 | 
					      'access_token': instance.accessToken,
 | 
				
			||||||
 | 
					      'refresh_token': instance.refreshToken,
 | 
				
			||||||
 | 
					      'expired_at': instance.expiredAt?.toIso8601String(),
 | 
				
			||||||
 | 
					      'available_at': instance.availableAt?.toIso8601String(),
 | 
				
			||||||
 | 
					      'last_grant_at': instance.lastGrantAt?.toIso8601String(),
 | 
				
			||||||
 | 
					      'nonce': instance.nonce,
 | 
				
			||||||
 | 
					      'client_id': instance.clientId,
 | 
				
			||||||
 | 
					      'account': instance.account.toJson(),
 | 
				
			||||||
 | 
					      'account_id': instance.accountId,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					AuthFactor _$AuthFactorFromJson(Map<String, dynamic> json) => AuthFactor(
 | 
				
			||||||
 | 
					      id: (json['id'] as num).toInt(),
 | 
				
			||||||
 | 
					      createdAt: DateTime.parse(json['created_at'] as String),
 | 
				
			||||||
 | 
					      updatedAt: DateTime.parse(json['updated_at'] as String),
 | 
				
			||||||
 | 
					      deletedAt: json['deleted_at'] == null
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : DateTime.parse(json['deleted_at'] as String),
 | 
				
			||||||
 | 
					      type: (json['type'] as num).toInt(),
 | 
				
			||||||
 | 
					      config: json['config'] as Map<String, dynamic>?,
 | 
				
			||||||
 | 
					      account: Account.fromJson(json['account'] as Map<String, dynamic>),
 | 
				
			||||||
 | 
					      accountId: (json['account_id'] as num).toInt(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Map<String, dynamic> _$AuthFactorToJson(AuthFactor instance) =>
 | 
				
			||||||
 | 
					    <String, dynamic>{
 | 
				
			||||||
 | 
					      'id': instance.id,
 | 
				
			||||||
 | 
					      'created_at': instance.createdAt.toIso8601String(),
 | 
				
			||||||
 | 
					      'updated_at': instance.updatedAt.toIso8601String(),
 | 
				
			||||||
 | 
					      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
				
			||||||
 | 
					      'type': instance.type,
 | 
				
			||||||
 | 
					      'config': instance.config,
 | 
				
			||||||
 | 
					      'account': instance.account.toJson(),
 | 
				
			||||||
 | 
					      'account_id': instance.accountId,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
@@ -9,6 +9,7 @@ import 'package:get/get_connect/http/src/request/request.dart';
 | 
				
			|||||||
import 'package:solian/background.dart';
 | 
					import 'package:solian/background.dart';
 | 
				
			||||||
import 'package:solian/exceptions/request.dart';
 | 
					import 'package:solian/exceptions/request.dart';
 | 
				
			||||||
import 'package:solian/exceptions/unauthorized.dart';
 | 
					import 'package:solian/exceptions/unauthorized.dart';
 | 
				
			||||||
 | 
					import 'package:solian/models/auth.dart';
 | 
				
			||||||
import 'package:solian/providers/database/database.dart';
 | 
					import 'package:solian/providers/database/database.dart';
 | 
				
			||||||
import 'package:solian/providers/websocket.dart';
 | 
					import 'package:solian/providers/websocket.dart';
 | 
				
			||||||
import 'package:solian/services.dart';
 | 
					import 'package:solian/services.dart';
 | 
				
			||||||
@@ -148,27 +149,13 @@ class AuthProvider extends GetConnect {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  Future<TokenSet> signin(
 | 
					  Future<TokenSet> signin(
 | 
				
			||||||
    BuildContext context,
 | 
					    BuildContext context,
 | 
				
			||||||
    String username,
 | 
					    AuthTicket ticket,
 | 
				
			||||||
    String password,
 | 
					 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    userProfile.value = null;
 | 
					    userProfile.value = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final client = ServiceFinder.configureClient('auth');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Create ticket
 | 
					 | 
				
			||||||
    final resp = await client.post('/auth', {
 | 
					 | 
				
			||||||
      'username': username,
 | 
					 | 
				
			||||||
      'password': password,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    if (resp.statusCode != 200) {
 | 
					 | 
				
			||||||
      throw RequestException(resp);
 | 
					 | 
				
			||||||
    } else if (resp.body['is_finished'] == false) {
 | 
					 | 
				
			||||||
      throw RiskyAuthenticateException(resp.body['ticket']['id']);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Assign token
 | 
					    // Assign token
 | 
				
			||||||
    final tokenResp = await post('/auth/token', {
 | 
					    final tokenResp = await post('/auth/token', {
 | 
				
			||||||
      'code': resp.body['ticket']['grant_token'],
 | 
					      'code': ticket.grantToken!,
 | 
				
			||||||
      'grant_type': 'grant_token',
 | 
					      'grant_type': 'grant_token',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    if (tokenResp.statusCode != 200) {
 | 
					    if (tokenResp.statusCode != 200) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,15 @@
 | 
				
			|||||||
 | 
					import 'package:animations/animations.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
import 'package:protocol_handler/protocol_handler.dart';
 | 
					 | 
				
			||||||
import 'package:solian/background.dart';
 | 
					import 'package:solian/background.dart';
 | 
				
			||||||
 | 
					import 'package:solian/exceptions/request.dart';
 | 
				
			||||||
import 'package:solian/exts.dart';
 | 
					import 'package:solian/exts.dart';
 | 
				
			||||||
import 'package:solian/providers/websocket.dart';
 | 
					import 'package:solian/models/auth.dart';
 | 
				
			||||||
import 'package:solian/providers/auth.dart';
 | 
					import 'package:solian/providers/auth.dart';
 | 
				
			||||||
 | 
					import 'package:solian/providers/websocket.dart';
 | 
				
			||||||
import 'package:solian/services.dart';
 | 
					import 'package:solian/services.dart';
 | 
				
			||||||
import 'package:solian/widgets/sized_container.dart';
 | 
					import 'package:solian/widgets/sized_container.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher.dart';
 | 
					 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SignInScreen extends StatefulWidget {
 | 
					class SignInScreen extends StatefulWidget {
 | 
				
			||||||
  const SignInScreen({super.key});
 | 
					  const SignInScreen({super.key});
 | 
				
			||||||
@@ -18,12 +18,28 @@ class SignInScreen extends StatefulWidget {
 | 
				
			|||||||
  State<SignInScreen> createState() => _SignInScreenState();
 | 
					  State<SignInScreen> createState() => _SignInScreenState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _SignInScreenState extends State<SignInScreen> with ProtocolListener {
 | 
					class _SignInScreenState extends State<SignInScreen> {
 | 
				
			||||||
  bool _isBusy = false;
 | 
					  bool _isBusy = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  AuthTicket? _currentTicket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<AuthFactor>? _factors;
 | 
				
			||||||
 | 
					  int? _factorPicked;
 | 
				
			||||||
 | 
					  int? _factorPickedType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int _period = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final _usernameController = TextEditingController();
 | 
					  final _usernameController = TextEditingController();
 | 
				
			||||||
  final _passwordController = TextEditingController();
 | 
					  final _passwordController = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
 | 
				
			||||||
 | 
					    0: ('authFactorPassword'.tr, Icons.password, false),
 | 
				
			||||||
 | 
					    1: ('authFactorEmail'.tr, Icons.email, true),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Color get _unFocusColor =>
 | 
				
			||||||
 | 
					      Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _requestResetPassword() async {
 | 
					  void _requestResetPassword() async {
 | 
				
			||||||
    final username = _usernameController.value.text;
 | 
					    final username = _usernameController.value.text;
 | 
				
			||||||
    if (username.isEmpty) {
 | 
					    if (username.isEmpty) {
 | 
				
			||||||
@@ -54,78 +70,141 @@ class _SignInScreenState extends State<SignInScreen> with ProtocolListener {
 | 
				
			|||||||
    context.showModalDialog('done'.tr, 'signinResetPasswordSent'.tr);
 | 
					    context.showModalDialog('done'.tr, 'signinResetPasswordSent'.tr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _performAction() async {
 | 
					  void _performNewTicket() async {
 | 
				
			||||||
    final AuthProvider auth = Get.find();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final username = _usernameController.value.text;
 | 
					    final username = _usernameController.value.text;
 | 
				
			||||||
    final password = _passwordController.value.text;
 | 
					    if (username.isEmpty) return;
 | 
				
			||||||
    if (username.isEmpty || password.isEmpty) return;
 | 
					
 | 
				
			||||||
 | 
					    final client = ServiceFinder.configureClient('auth');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await auth.signin(context, username, password);
 | 
					      // Create ticket
 | 
				
			||||||
      await Future.delayed(const Duration(milliseconds: 250), () async {
 | 
					      final resp = await client.post('/auth', {
 | 
				
			||||||
        await auth.refreshAuthorizeStatus();
 | 
					        'username': username,
 | 
				
			||||||
        await auth.refreshUserProfile();
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } on RiskyAuthenticateException catch (e) {
 | 
					      if (resp.statusCode != 200) {
 | 
				
			||||||
      showDialog(
 | 
					        throw RequestException(resp);
 | 
				
			||||||
        context: context,
 | 
					      } else {
 | 
				
			||||||
        builder: (context) {
 | 
					        final result = AuthResult.fromJson(resp.body);
 | 
				
			||||||
          return AlertDialog(
 | 
					        _currentTicket = result.ticket;
 | 
				
			||||||
            title: Text('riskDetection'.tr),
 | 
					      }
 | 
				
			||||||
            content: Text('signinRiskDetected'.tr),
 | 
					
 | 
				
			||||||
            actions: [
 | 
					      // Pull factors
 | 
				
			||||||
              TextButton(
 | 
					      final factorResp = await client.get('/auth/factors',
 | 
				
			||||||
                child: Text('next'.tr),
 | 
					          query: {'ticketId': _currentTicket!.id.toString()});
 | 
				
			||||||
                onPressed: () {
 | 
					      if (factorResp.statusCode != 200) {
 | 
				
			||||||
                  const redirect = 'solink://auth?status=done';
 | 
					        throw RequestException(factorResp);
 | 
				
			||||||
                  launchUrlString(
 | 
					      } else {
 | 
				
			||||||
                    ServiceFinder.buildUrl('capital',
 | 
					        final result = List<AuthFactor>.from(
 | 
				
			||||||
                        '/auth/mfa?redirect_uri=$redirect&ticketId=${e.ticketId}'),
 | 
					          factorResp.body.map((x) => AuthFactor.fromJson(x)),
 | 
				
			||||||
                    mode: LaunchMode.inAppWebView,
 | 
					        );
 | 
				
			||||||
                  );
 | 
					        _factors = result;
 | 
				
			||||||
                  Navigator.pop(context);
 | 
					      }
 | 
				
			||||||
                },
 | 
					
 | 
				
			||||||
              )
 | 
					      setState(() => _period++);
 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      context.showErrorDialog(e);
 | 
					      context.showErrorDialog(e);
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      setState(() => _isBusy = false);
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Get.find<WebSocketProvider>().registerPushNotifications();
 | 
					 | 
				
			||||||
    autoConfigureBackgroundNotificationService();
 | 
					 | 
				
			||||||
    autoStartBackgroundNotificationService();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Navigator.pop(context, true);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  void _performGetFactorCode() async {
 | 
				
			||||||
  void initState() {
 | 
					    if (_factorPicked == null) return;
 | 
				
			||||||
    protocolHandler.addListener(this);
 | 
					
 | 
				
			||||||
    super.initState();
 | 
					    final client = ServiceFinder.configureClient('auth');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // Request one-time-password code
 | 
				
			||||||
 | 
					      final resp = await client.post('/auth/factors/$_factorPicked', {});
 | 
				
			||||||
 | 
					      if (resp.statusCode != 200 && resp.statusCode != 204) {
 | 
				
			||||||
 | 
					        throw RequestException(resp);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        _factorPickedType = _factors!
 | 
				
			||||||
 | 
					            .where(
 | 
				
			||||||
 | 
					              (x) => x.id == _factorPicked,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .first
 | 
				
			||||||
 | 
					            .type;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setState(() => _period++);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      context.showErrorDialog(e);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  void _performCheckTicket() async {
 | 
				
			||||||
  void dispose() {
 | 
					    final AuthProvider auth = Get.find();
 | 
				
			||||||
    protocolHandler.removeListener(this);
 | 
					
 | 
				
			||||||
    super.dispose();
 | 
					    final password = _passwordController.value.text;
 | 
				
			||||||
 | 
					    if (password.isEmpty) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final client = ServiceFinder.configureClient('auth');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // Check ticket
 | 
				
			||||||
 | 
					      final resp = await client.patch('/auth', {
 | 
				
			||||||
 | 
					        'ticket_id': _currentTicket!.id,
 | 
				
			||||||
 | 
					        'factor_id': _factorPicked!,
 | 
				
			||||||
 | 
					        'code': password,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (resp.statusCode != 200) {
 | 
				
			||||||
 | 
					        throw RequestException(resp);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final result = AuthResult.fromJson(resp.body);
 | 
				
			||||||
 | 
					      _currentTicket = result.ticket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Finish sign in if possible
 | 
				
			||||||
 | 
					      if (result.isFinished) {
 | 
				
			||||||
 | 
					        await auth.signin(context, _currentTicket!);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await Future.delayed(const Duration(milliseconds: 250), () async {
 | 
				
			||||||
 | 
					          await auth.refreshAuthorizeStatus();
 | 
				
			||||||
 | 
					          await auth.refreshUserProfile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          Get.find<WebSocketProvider>().registerPushNotifications();
 | 
				
			||||||
 | 
					          autoConfigureBackgroundNotificationService();
 | 
				
			||||||
 | 
					          autoStartBackgroundNotificationService();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          Navigator.pop(context, true);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Skip the first step
 | 
				
			||||||
 | 
					        _factorPicked = null;
 | 
				
			||||||
 | 
					        _factorPickedType = null;
 | 
				
			||||||
 | 
					        setState(() => _period += 2);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      context.showErrorDialog(e);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  void _previousStep() {
 | 
				
			||||||
  void onProtocolUrlReceived(String url) {
 | 
					    assert(_period > 0);
 | 
				
			||||||
    final uri = url.replaceFirst('solink://', '');
 | 
					    switch (_period % 3) {
 | 
				
			||||||
    if (uri == 'auth?status=done') {
 | 
					      case 1:
 | 
				
			||||||
      closeInAppWebView();
 | 
					        _currentTicket = null;
 | 
				
			||||||
      _performAction();
 | 
					        _factors = null;
 | 
				
			||||||
 | 
					        _factorPicked = null;
 | 
				
			||||||
 | 
					      case 2:
 | 
				
			||||||
 | 
					        _passwordController.clear();
 | 
				
			||||||
 | 
					        _factorPickedType = null;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        setState(() => _period--);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -135,72 +214,237 @@ class _SignInScreenState extends State<SignInScreen> with ProtocolListener {
 | 
				
			|||||||
      color: Theme.of(context).colorScheme.surface,
 | 
					      color: Theme.of(context).colorScheme.surface,
 | 
				
			||||||
      child: CenteredContainer(
 | 
					      child: CenteredContainer(
 | 
				
			||||||
        maxWidth: 360,
 | 
					        maxWidth: 360,
 | 
				
			||||||
        child: Column(
 | 
					        child: PageTransitionSwitcher(
 | 
				
			||||||
          mainAxisSize: MainAxisSize.min,
 | 
					          transitionBuilder: (
 | 
				
			||||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
					            Widget child,
 | 
				
			||||||
          children: [
 | 
					            Animation<double> primaryAnimation,
 | 
				
			||||||
            ClipRRect(
 | 
					            Animation<double> secondaryAnimation,
 | 
				
			||||||
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					          ) {
 | 
				
			||||||
              child: Image.asset('assets/logo.png', width: 64, height: 64),
 | 
					            return SharedAxisTransition(
 | 
				
			||||||
            ).paddingOnly(bottom: 4),
 | 
					              animation: primaryAnimation,
 | 
				
			||||||
            Text(
 | 
					              secondaryAnimation: secondaryAnimation,
 | 
				
			||||||
              'signinGreeting'.tr,
 | 
					              transitionType: SharedAxisTransitionType.horizontal,
 | 
				
			||||||
              style: const TextStyle(
 | 
					              child: child,
 | 
				
			||||||
                fontSize: 28,
 | 
					            );
 | 
				
			||||||
                fontWeight: FontWeight.w900,
 | 
					          },
 | 
				
			||||||
              ),
 | 
					          child: switch (_period % 3) {
 | 
				
			||||||
            ).paddingOnly(left: 4, bottom: 16),
 | 
					            1 => Column(
 | 
				
			||||||
            TextField(
 | 
					                key: const ValueKey<int>(1),
 | 
				
			||||||
              autocorrect: false,
 | 
					                mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
              enableSuggestions: false,
 | 
					                crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
              controller: _usernameController,
 | 
					                children: [
 | 
				
			||||||
              autofillHints: const [AutofillHints.username],
 | 
					                  ClipRRect(
 | 
				
			||||||
              decoration: InputDecoration(
 | 
					                    borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
                isDense: true,
 | 
					                    child:
 | 
				
			||||||
                border: const OutlineInputBorder(),
 | 
					                        Image.asset('assets/logo.png', width: 64, height: 64),
 | 
				
			||||||
                labelText: 'username'.tr,
 | 
					                  ).paddingOnly(bottom: 8, left: 4),
 | 
				
			||||||
              ),
 | 
					                  Text(
 | 
				
			||||||
              onTapOutside: (_) =>
 | 
					                    'signinPickFactor'.tr,
 | 
				
			||||||
                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
					                    style: const TextStyle(
 | 
				
			||||||
            ),
 | 
					                      fontSize: 28,
 | 
				
			||||||
            const Gap(12),
 | 
					                      fontWeight: FontWeight.w900,
 | 
				
			||||||
            TextField(
 | 
					                    ),
 | 
				
			||||||
              obscureText: true,
 | 
					                  ).paddingOnly(left: 4, bottom: 16),
 | 
				
			||||||
              autocorrect: false,
 | 
					                  Card(
 | 
				
			||||||
              enableSuggestions: false,
 | 
					                    margin: const EdgeInsets.symmetric(vertical: 4),
 | 
				
			||||||
              autofillHints: const [AutofillHints.password],
 | 
					                    child: Column(
 | 
				
			||||||
              controller: _passwordController,
 | 
					                      children: _factors
 | 
				
			||||||
              decoration: InputDecoration(
 | 
					                              ?.map(
 | 
				
			||||||
                isDense: true,
 | 
					                                (x) => CheckboxListTile(
 | 
				
			||||||
                border: const OutlineInputBorder(),
 | 
					                                  shape: const RoundedRectangleBorder(
 | 
				
			||||||
                labelText: 'password'.tr,
 | 
					                                    borderRadius: BorderRadius.all(
 | 
				
			||||||
              ),
 | 
					                                      Radius.circular(8),
 | 
				
			||||||
              onTapOutside: (_) =>
 | 
					                                    ),
 | 
				
			||||||
                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
					                                  ),
 | 
				
			||||||
              onSubmitted: (_) => _performAction(),
 | 
					                                  secondary: Icon(
 | 
				
			||||||
            ),
 | 
					                                    _factorLabelMap[x.type]?.$2 ??
 | 
				
			||||||
            const Gap(12),
 | 
					                                        Icons.question_mark,
 | 
				
			||||||
            Row(
 | 
					                                  ),
 | 
				
			||||||
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
					                                  title: Text(
 | 
				
			||||||
              children: [
 | 
					                                    _factorLabelMap[x.type]?.$1 ?? 'unknown'.tr,
 | 
				
			||||||
                TextButton(
 | 
					                                  ),
 | 
				
			||||||
                  onPressed: _isBusy ? null : () => _requestResetPassword(),
 | 
					                                  enabled: !_currentTicket!.factorTrail
 | 
				
			||||||
                  style: TextButton.styleFrom(foregroundColor: Colors.grey),
 | 
					                                      .contains(x.id),
 | 
				
			||||||
                  child: Text('forgotPassword'.tr),
 | 
					                                  value: _factorPicked == x.id,
 | 
				
			||||||
                ),
 | 
					                                  onChanged: (value) {
 | 
				
			||||||
                TextButton(
 | 
					                                    if (value == true) {
 | 
				
			||||||
                  onPressed: _isBusy ? null : () => _performAction(),
 | 
					                                      setState(() => _factorPicked = x.id);
 | 
				
			||||||
                  child: Row(
 | 
					                                    }
 | 
				
			||||||
                    mainAxisSize: MainAxisSize.min,
 | 
					                                  },
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              )
 | 
				
			||||||
 | 
					                              .toList() ??
 | 
				
			||||||
 | 
					                          List.empty(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  Text(
 | 
				
			||||||
 | 
					                    'signinMultiFactor'.trParams(
 | 
				
			||||||
 | 
					                      {'n': _currentTicket!.stepRemain.toString()},
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    style: TextStyle(color: _unFocusColor, fontSize: 12),
 | 
				
			||||||
 | 
					                  ).paddingOnly(left: 16, right: 16),
 | 
				
			||||||
 | 
					                  const Gap(12),
 | 
				
			||||||
 | 
					                  Row(
 | 
				
			||||||
 | 
					                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
                    children: [
 | 
					                    children: [
 | 
				
			||||||
                      Text('next'.tr),
 | 
					                      TextButton(
 | 
				
			||||||
                      const Icon(Icons.chevron_right),
 | 
					                        onPressed: _isBusy ? null : () => _previousStep(),
 | 
				
			||||||
 | 
					                        style:
 | 
				
			||||||
 | 
					                            TextButton.styleFrom(foregroundColor: Colors.grey),
 | 
				
			||||||
 | 
					                        child: Row(
 | 
				
			||||||
 | 
					                          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            const Icon(Icons.chevron_left),
 | 
				
			||||||
 | 
					                            Text('prev'.tr),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      TextButton(
 | 
				
			||||||
 | 
					                        onPressed:
 | 
				
			||||||
 | 
					                            _isBusy ? null : () => _performGetFactorCode(),
 | 
				
			||||||
 | 
					                        child: Row(
 | 
				
			||||||
 | 
					                          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            Text('next'.tr),
 | 
				
			||||||
 | 
					                            const Icon(Icons.chevron_right),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ],
 | 
				
			||||||
              ],
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            2 => Column(
 | 
				
			||||||
          ],
 | 
					                key: const ValueKey<int>(2),
 | 
				
			||||||
 | 
					                mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  ClipRRect(
 | 
				
			||||||
 | 
					                    borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					                    child:
 | 
				
			||||||
 | 
					                        Image.asset('assets/logo.png', width: 64, height: 64),
 | 
				
			||||||
 | 
					                  ).paddingOnly(bottom: 8, left: 4),
 | 
				
			||||||
 | 
					                  Text(
 | 
				
			||||||
 | 
					                    'signinEnterPassword'.tr,
 | 
				
			||||||
 | 
					                    style: const TextStyle(
 | 
				
			||||||
 | 
					                      fontSize: 28,
 | 
				
			||||||
 | 
					                      fontWeight: FontWeight.w900,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ).paddingOnly(left: 4, bottom: 16),
 | 
				
			||||||
 | 
					                  TextField(
 | 
				
			||||||
 | 
					                    autocorrect: false,
 | 
				
			||||||
 | 
					                    enableSuggestions: false,
 | 
				
			||||||
 | 
					                    controller: _passwordController,
 | 
				
			||||||
 | 
					                    obscureText: true,
 | 
				
			||||||
 | 
					                    autofillHints: [
 | 
				
			||||||
 | 
					                      (_factorLabelMap[_factorPickedType]?.$3 ?? true)
 | 
				
			||||||
 | 
					                          ? AutofillHints.password
 | 
				
			||||||
 | 
					                          : AutofillHints.oneTimeCode
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    decoration: InputDecoration(
 | 
				
			||||||
 | 
					                      isDense: true,
 | 
				
			||||||
 | 
					                      border: const OutlineInputBorder(),
 | 
				
			||||||
 | 
					                      labelText:
 | 
				
			||||||
 | 
					                          (_factorLabelMap[_factorPickedType]?.$3 ?? true)
 | 
				
			||||||
 | 
					                              ? 'passwordOneTime'.tr
 | 
				
			||||||
 | 
					                              : 'password'.tr,
 | 
				
			||||||
 | 
					                      helperText:
 | 
				
			||||||
 | 
					                          (_factorLabelMap[_factorPickedType]?.$3 ?? true)
 | 
				
			||||||
 | 
					                              ? 'passwordOneTimeInputHint'.tr
 | 
				
			||||||
 | 
					                              : 'passwordInputHint'.tr,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    onTapOutside: (_) =>
 | 
				
			||||||
 | 
					                        FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                    onSubmitted: _isBusy ? null : (_) => _performCheckTicket(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  const Gap(12),
 | 
				
			||||||
 | 
					                  Row(
 | 
				
			||||||
 | 
					                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      TextButton(
 | 
				
			||||||
 | 
					                        onPressed: _isBusy ? null : () => _previousStep(),
 | 
				
			||||||
 | 
					                        style:
 | 
				
			||||||
 | 
					                            TextButton.styleFrom(foregroundColor: Colors.grey),
 | 
				
			||||||
 | 
					                        child: Row(
 | 
				
			||||||
 | 
					                          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            const Icon(Icons.chevron_left),
 | 
				
			||||||
 | 
					                            Text('prev'.tr),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      TextButton(
 | 
				
			||||||
 | 
					                        onPressed: _isBusy ? null : () => _performCheckTicket(),
 | 
				
			||||||
 | 
					                        child: Row(
 | 
				
			||||||
 | 
					                          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            Text('next'.tr),
 | 
				
			||||||
 | 
					                            const Icon(Icons.chevron_right),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            _ => Column(
 | 
				
			||||||
 | 
					                key: const ValueKey<int>(0),
 | 
				
			||||||
 | 
					                mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  ClipRRect(
 | 
				
			||||||
 | 
					                    borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					                    child:
 | 
				
			||||||
 | 
					                        Image.asset('assets/logo.png', width: 64, height: 64),
 | 
				
			||||||
 | 
					                  ).paddingOnly(bottom: 8, left: 4),
 | 
				
			||||||
 | 
					                  Text(
 | 
				
			||||||
 | 
					                    'signinGreeting'.tr,
 | 
				
			||||||
 | 
					                    style: const TextStyle(
 | 
				
			||||||
 | 
					                      fontSize: 28,
 | 
				
			||||||
 | 
					                      fontWeight: FontWeight.w900,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ).paddingOnly(left: 4, bottom: 16),
 | 
				
			||||||
 | 
					                  TextField(
 | 
				
			||||||
 | 
					                    autocorrect: false,
 | 
				
			||||||
 | 
					                    enableSuggestions: false,
 | 
				
			||||||
 | 
					                    controller: _usernameController,
 | 
				
			||||||
 | 
					                    autofillHints: const [AutofillHints.username],
 | 
				
			||||||
 | 
					                    decoration: InputDecoration(
 | 
				
			||||||
 | 
					                      isDense: true,
 | 
				
			||||||
 | 
					                      border: const OutlineInputBorder(),
 | 
				
			||||||
 | 
					                      labelText: 'username'.tr,
 | 
				
			||||||
 | 
					                      helperText: 'usernameInputHint'.tr,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    onTapOutside: (_) =>
 | 
				
			||||||
 | 
					                        FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                    onSubmitted: _isBusy ? null : (_) => _performNewTicket(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  const Gap(12),
 | 
				
			||||||
 | 
					                  Row(
 | 
				
			||||||
 | 
					                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      TextButton(
 | 
				
			||||||
 | 
					                        onPressed:
 | 
				
			||||||
 | 
					                            _isBusy ? null : () => _requestResetPassword(),
 | 
				
			||||||
 | 
					                        style:
 | 
				
			||||||
 | 
					                            TextButton.styleFrom(foregroundColor: Colors.grey),
 | 
				
			||||||
 | 
					                        child: Text('forgotPassword'.tr),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      TextButton(
 | 
				
			||||||
 | 
					                        onPressed: _isBusy ? null : () => _performNewTicket(),
 | 
				
			||||||
 | 
					                        child: Row(
 | 
				
			||||||
 | 
					                          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            Text('next'.tr),
 | 
				
			||||||
 | 
					                            const Icon(Icons.chevron_right),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -73,7 +73,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
 | 
				
			|||||||
            ClipRRect(
 | 
					            ClipRRect(
 | 
				
			||||||
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
              child: Image.asset('assets/logo.png', width: 64, height: 64),
 | 
					              child: Image.asset('assets/logo.png', width: 64, height: 64),
 | 
				
			||||||
            ).paddingOnly(bottom: 4),
 | 
					            ).paddingOnly(bottom: 8, left: 4),
 | 
				
			||||||
            Text(
 | 
					            Text(
 | 
				
			||||||
              'signupGreeting'.tr,
 | 
					              'signupGreeting'.tr,
 | 
				
			||||||
              style: const TextStyle(
 | 
					              style: const TextStyle(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
abstract class ServiceFinder {
 | 
					abstract class ServiceFinder {
 | 
				
			||||||
  static const bool devFlag = false;
 | 
					  static const bool devFlag = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static const String dealerUrl =
 | 
					  static const String dealerUrl =
 | 
				
			||||||
      devFlag ? 'http://localhost:8442' : 'https://api.sn.solsynth.dev';
 | 
					      devFlag ? 'http://localhost:8442' : 'https://api.sn.solsynth.dev';
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user