add device info at login

This commit is contained in:
Efril 2026-05-12 02:00:38 +07:00
parent 9b6e9c591d
commit a4a12c6763
7 changed files with 159 additions and 6 deletions

View File

@ -4,6 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import '../../../common/utils/device_info_service.dart';
import '../../../common/utils/fcm_service.dart';
import '../../../domain/auth/auth.dart'; import '../../../domain/auth/auth.dart';
part 'login_form_event.dart'; part 'login_form_event.dart';
@ -13,7 +15,11 @@ part 'login_form_bloc.freezed.dart';
@injectable @injectable
class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> { class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> {
final IAuthRepository _repository; final IAuthRepository _repository;
LoginFormBloc(this._repository) : super(LoginFormState.initial()) { final DeviceInfoService _deviceInfoService;
final FcmService _fcmService;
LoginFormBloc(this._repository, this._deviceInfoService, this._fcmService)
: super(LoginFormState.initial()) {
on<LoginFormEvent>(_onLoginFormEvent); on<LoginFormEvent>(_onLoginFormEvent);
} }
@ -36,9 +42,25 @@ class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> {
final passwordValid = state.password.isNotEmpty; final passwordValid = state.password.isNotEmpty;
if (emailValid && passwordValid) { if (emailValid && passwordValid) {
// Ambil device info dan FCM token secara paralel
final results = await Future.wait([
_deviceInfoService.getDeviceInfo(),
_fcmService.getToken(),
]);
final deviceInfo = results[0] as DeviceInfo;
final fcmToken = results[1] as String?;
failureOrAuth = await _repository.login( failureOrAuth = await _repository.login(
email: state.email, email: state.email,
password: state.password, password: state.password,
deviceId: deviceInfo.deviceId,
deviceName: deviceInfo.deviceName,
deviceType: deviceInfo.deviceType,
platform: deviceInfo.platform,
osVersion: deviceInfo.osVersion,
appVersion: deviceInfo.appVersion,
fcmToken: fcmToken,
); );
emit( emit(
state.copyWith( state.copyWith(

View File

@ -0,0 +1,82 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:injectable/injectable.dart';
import 'package:package_info_plus/package_info_plus.dart';
class DeviceInfo {
final String deviceId;
final String deviceName;
final String deviceType;
final String platform;
final String osVersion;
final String appVersion;
const DeviceInfo({
required this.deviceId,
required this.deviceName,
required this.deviceType,
required this.platform,
required this.osVersion,
required this.appVersion,
});
Map<String, dynamic> toJson() => {
'device_id': deviceId,
'device_name': deviceName,
'device_type': deviceType,
'platform': platform,
'os_version': osVersion,
'app_version': appVersion,
};
}
@lazySingleton
class DeviceInfoService {
final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin();
Future<DeviceInfo> getDeviceInfo() async {
final packageInfo = await PackageInfo.fromPlatform();
final appVersion = packageInfo.version;
if (Platform.isAndroid) {
final info = await _deviceInfo.androidInfo;
return DeviceInfo(
deviceId: info.id,
deviceName: '${info.manufacturer} ${info.model}',
deviceType: _resolveDeviceType(info.model),
platform: 'android',
osVersion: 'Android ${info.version.release}',
appVersion: appVersion,
);
} else if (Platform.isIOS) {
final info = await _deviceInfo.iosInfo;
return DeviceInfo(
deviceId: info.identifierForVendor ?? '',
deviceName: info.name,
deviceType: _resolveDeviceType(info.model),
platform: 'ios',
osVersion: '${info.systemName} ${info.systemVersion}',
appVersion: appVersion,
);
}
// Fallback (web/desktop tidak dipakai tapi aman)
return DeviceInfo(
deviceId: 'unknown',
deviceName: 'unknown',
deviceType: 'desktop',
platform: 'web',
osVersion: 'unknown',
appVersion: appVersion,
);
}
/// Tentukan device_type berdasarkan model name.
/// Nilai valid: 'mobile' | 'tablet' | 'desktop'
String _resolveDeviceType(String model) {
final lower = model.toLowerCase();
if (lower.contains('ipad') || lower.contains('tablet')) return 'tablet';
return 'mobile';
}
}

View File

@ -4,6 +4,13 @@ abstract class IAuthRepository {
Future<Either<AuthFailure, Auth>> login({ Future<Either<AuthFailure, Auth>> login({
required String email, required String email,
required String password, required String password,
required String deviceId,
required String deviceName,
required String deviceType,
required String platform,
required String osVersion,
required String appVersion,
String? fcmToken,
}); });
Future<bool> hasToken(); Future<bool> hasToken();
Future<Either<AuthFailure, User>> currentUser(); Future<Either<AuthFailure, User>> currentUser();

View File

@ -21,11 +21,28 @@ class AuthRemoteDataProvider {
Future<DC<AuthFailure, AuthDto>> login({ Future<DC<AuthFailure, AuthDto>> login({
required String email, required String email,
required String password, required String password,
required String deviceId,
required String deviceName,
required String deviceType,
required String platform,
required String osVersion,
required String appVersion,
String? fcmToken,
}) async { }) async {
try { try {
final response = await _apiClient.post( final response = await _apiClient.post(
ApiPath.login, ApiPath.login,
data: {'email': email, 'password': password}, data: {
'email': email,
'password': password,
'device_id': deviceId,
'device_name': deviceName,
'device_type': deviceType,
'platform': platform,
'os_version': osVersion,
'app_version': appVersion,
if (fcmToken != null) 'fcm_token': fcmToken,
},
); );
if (response.data['code'] == 401) { if (response.data['code'] == 401) {

View File

@ -21,11 +21,25 @@ class AuthRepository implements IAuthRepository {
Future<Either<AuthFailure, Auth>> login({ Future<Either<AuthFailure, Auth>> login({
required String email, required String email,
required String password, required String password,
required String deviceId,
required String deviceName,
required String deviceType,
required String platform,
required String osVersion,
required String appVersion,
String? fcmToken,
}) async { }) async {
try { try {
final result = await _remoteDataProvider.login( final result = await _remoteDataProvider.login(
email: email, email: email,
password: password, password: password,
deviceId: deviceId,
deviceName: deviceName,
deviceType: deviceType,
platform: platform,
osVersion: osVersion,
appVersion: appVersion,
fcmToken: fcmToken,
); );
if (result.hasError) { if (result.hasError) {

View File

@ -59,6 +59,8 @@ import 'package:apskel_owner_flutter/common/di/di_shared_preferences.dart'
as _i402; as _i402;
import 'package:apskel_owner_flutter/common/network/network_client.dart' import 'package:apskel_owner_flutter/common/network/network_client.dart'
as _i543; as _i543;
import 'package:apskel_owner_flutter/common/utils/device_info_service.dart'
as _i902;
import 'package:apskel_owner_flutter/common/utils/fcm_service.dart' as _i179; import 'package:apskel_owner_flutter/common/utils/fcm_service.dart' as _i179;
import 'package:apskel_owner_flutter/domain/analytic/repositories/i_analytic_repository.dart' import 'package:apskel_owner_flutter/domain/analytic/repositories/i_analytic_repository.dart'
as _i477; as _i477;
@ -146,6 +148,7 @@ extension GetItInjectableX on _i174.GetIt {
preResolve: true, preResolve: true,
); );
gh.lazySingleton<_i179.FcmService>(() => _i179.FcmService()); gh.lazySingleton<_i179.FcmService>(() => _i179.FcmService());
gh.lazySingleton<_i902.DeviceInfoService>(() => _i902.DeviceInfoService());
gh.lazySingleton<_i543.NetworkClient>( gh.lazySingleton<_i543.NetworkClient>(
() => _i543.NetworkClient(gh<_i895.Connectivity>()), () => _i543.NetworkClient(gh<_i895.Connectivity>()),
); );
@ -261,9 +264,6 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i945.AuthBloc>( gh.factory<_i945.AuthBloc>(
() => _i945.AuthBloc(gh<_i49.IAuthRepository>()), () => _i945.AuthBloc(gh<_i49.IAuthRepository>()),
); );
gh.factory<_i775.LoginFormBloc>(
() => _i775.LoginFormBloc(gh<_i49.IAuthRepository>()),
);
gh.factory<_i574.LogoutFormBloc>( gh.factory<_i574.LogoutFormBloc>(
() => _i574.LogoutFormBloc(gh<_i49.IAuthRepository>()), () => _i574.LogoutFormBloc(gh<_i49.IAuthRepository>()),
); );
@ -276,6 +276,13 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i147.UserEditFormBloc>( gh.factory<_i147.UserEditFormBloc>(
() => _i147.UserEditFormBloc(gh<_i635.IUserRepository>()), () => _i147.UserEditFormBloc(gh<_i635.IUserRepository>()),
); );
gh.factory<_i775.LoginFormBloc>(
() => _i775.LoginFormBloc(
gh<_i49.IAuthRepository>(),
gh<_i902.DeviceInfoService>(),
gh<_i179.FcmService>(),
),
);
gh.factory<_i346.InventoryReportBloc>( gh.factory<_i346.InventoryReportBloc>(
() => _i346.InventoryReportBloc( () => _i346.InventoryReportBloc(
gh<_i477.IAnalyticRepository>(), gh<_i477.IAnalyticRepository>(),

View File

@ -14,11 +14,15 @@ import 'package:flutter/widgets.dart';
class $AssetsImagesGen { class $AssetsImagesGen {
const $AssetsImagesGen(); const $AssetsImagesGen();
/// File path: assets/images/ic_notification.png
AssetGenImage get icNotification =>
const AssetGenImage('assets/images/ic_notification.png');
/// File path: assets/images/logo.png /// File path: assets/images/logo.png
AssetGenImage get logo => const AssetGenImage('assets/images/logo.png'); AssetGenImage get logo => const AssetGenImage('assets/images/logo.png');
/// List of all assets /// List of all assets
List<AssetGenImage> get values => [logo]; List<AssetGenImage> get values => [icNotification, logo];
} }
class Assets { class Assets {