Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:privacy_gui/core/jnap/actions/better_action.dart';
import 'package:privacy_gui/core/jnap/command/base_command.dart';
import 'package:privacy_gui/core/jnap/models/firmware_update_settings.dart';
import 'package:privacy_gui/core/data/providers/firmware_update_provider.dart';
import 'package:privacy_gui/core/data/providers/polling_provider.dart';
import 'package:privacy_gui/core/jnap/router_repository.dart';
import 'package:privacy_gui/core/retry_strategy/retry.dart';
import 'package:privacy_gui/core/utils/logger.dart';
import 'package:privacy_gui/page/login/auto_parent/providers/auto_parent_first_login_state.dart';
import 'package:privacy_gui/page/login/auto_parent/services/auto_parent_first_login_service.dart';

final autoParentFirstLoginProvider = NotifierProvider.autoDispose<
AutoParentFirstLoginNotifier,
Expand Down Expand Up @@ -40,85 +36,17 @@ class AutoParentFirstLoginNotifier
}
}

// set userAcknowledgedAutoConfiguration to true
Future<void> setUserAcknowledgedAutoConfiguration() async {
final repo = ref.read(routerRepositoryProvider);
repo.send(
JNAPAction.setUserAcknowledgedAutoConfiguration,
fetchRemote: true,
cacheLevel: CacheLevel.noCache,
data: {},
auth: true,
);
}

// set firmware updatePolicy to [FirmwareUpdateSettings.firmwareUpdatePolicyAuto]
Future<void> setFirmwareUpdatePolicy() async {
final repo = ref.read(routerRepositoryProvider);
// Get current firmware update settings
final firmwareUpdateSettings = await repo
.send(
JNAPAction.getFirmwareUpdateSettings,
fetchRemote: true,
auth: true,
)
.then((value) => value.output)
.then(
(output) => FirmwareUpdateSettings.fromMap(output).copyWith(
updatePolicy: FirmwareUpdateSettings.firmwareUpdatePolicyAuto),
)
.onError((error, stackTrace) {
return FirmwareUpdateSettings(
updatePolicy: FirmwareUpdateSettings.firmwareUpdatePolicyAuto,
autoUpdateWindow: FirmwareAutoUpdateWindow(
startMinute: 0,
durationMinutes: 240,
));
});

// enable auto firmware update
repo.send(
JNAPAction.setFirmwareUpdateSettings,
fetchRemote: true,
cacheLevel: CacheLevel.noCache,
data: firmwareUpdateSettings.toMap(),
auth: true,
);
}

// Check internet connection via JNAP with 10 attempts retries
Future<bool> checkInternetConnection() async {
final repo = ref.read(routerRepositoryProvider);
// make up to 5 attempts to check internet connection total 10 seconds
final retryStrategy = ExponentialBackoffRetryStrategy(
maxRetries: 5,
initialDelay: const Duration(seconds: 2),
maxDelay: const Duration(seconds: 2),
);
return retryStrategy.execute<bool>(() async {
final result = await repo.send(
JNAPAction.getInternetConnectionStatus,
fetchRemote: true,
auth: true,
);
logger.i('[FirstTime]: Internet connection status: ${result.output}');
final connectionStatus = result.output['connectionStatus'];
return connectionStatus == 'InternetConnected';
}, shouldRetry: (result) => !result).onError((error, stackTrace) {
logger.e('[FirstTime]: Error checking internet connection: $error');
return false;
});
}

Future<void> finishFirstTimeLogin([bool failCheck = false]) async {
final service = ref.read(autoParentFirstLoginServiceProvider);

// Keep userAcknowledgedAutoConfiguration to false if check firmware failed
if (!failCheck) {
// wait for internet connection
final isConnected = await checkInternetConnection();
final isConnected = await service.checkInternetConnection();
logger.i('[FirstTime]: Internet connection status: $isConnected');
await setUserAcknowledgedAutoConfiguration();
await service.setUserAcknowledgedAutoConfiguration();
}
// Set firmware update policy
await setFirmwareUpdatePolicy();
await service.setFirmwareUpdatePolicy();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:privacy_gui/core/errors/jnap_error_mapper.dart';
import 'package:privacy_gui/core/jnap/actions/better_action.dart';
import 'package:privacy_gui/core/jnap/command/base_command.dart';
import 'package:privacy_gui/core/jnap/models/firmware_update_settings.dart';
import 'package:privacy_gui/core/jnap/result/jnap_result.dart';
import 'package:privacy_gui/core/jnap/router_repository.dart';
import 'package:privacy_gui/core/retry_strategy/retry.dart';
import 'package:privacy_gui/core/utils/logger.dart';

/// Riverpod provider for AutoParentFirstLoginService
final autoParentFirstLoginServiceProvider =
Provider<AutoParentFirstLoginService>((ref) {
return AutoParentFirstLoginService(ref.watch(routerRepositoryProvider));
});

/// Stateless service for auto-parent first-time login operations.
///
/// Handles JNAP communication for:
/// - Setting user acknowledged auto configuration
/// - Configuring firmware update policy
/// - Checking internet connection status
///
/// Follows constitution.md Article VI Section 6.2:
/// - Handles all JNAP API communication
/// - Returns simple results (void, bool), not raw JNAP responses
/// - Stateless (no internal state)
/// - Dependencies injected via constructor
class AutoParentFirstLoginService {
AutoParentFirstLoginService(this._routerRepository);

final RouterRepository _routerRepository;

/// Sets userAcknowledgedAutoConfiguration flag on the router.
///
/// Awaits the JNAP response to ensure the operation completes.
///
/// JNAP Action: [JNAPAction.setUserAcknowledgedAutoConfiguration]
///
/// Returns: [Future<void>] completes when operation succeeds
///
/// Throws: [ServiceError] if operation fails
Future<void> setUserAcknowledgedAutoConfiguration() async {
try {
await _routerRepository.send(
JNAPAction.setUserAcknowledgedAutoConfiguration,
fetchRemote: true,
cacheLevel: CacheLevel.noCache,
data: {},
auth: true,
);
} on JNAPError catch (e) {
throw mapJnapErrorToServiceError(e);
}
}

/// Fetches current firmware update settings and enables auto-update policy.
///
/// If fetching current settings fails, uses default settings:
/// - updatePolicy: firmwareUpdatePolicyAuto
/// - autoUpdateWindow: startMinute=0, durationMinutes=240
///
/// JNAP Actions:
/// - GET: [JNAPAction.getFirmwareUpdateSettings]
/// - SET: [JNAPAction.setFirmwareUpdateSettings]
///
/// Returns: [Future<void>] completes when settings are saved
///
/// Throws: [ServiceError] if save operation fails
Future<void> setFirmwareUpdatePolicy() async {
// Get current firmware update settings
final firmwareUpdateSettings = await _routerRepository
.send(
JNAPAction.getFirmwareUpdateSettings,
fetchRemote: true,
auth: true,
)
.then((value) => value.output)
.then(
(output) => FirmwareUpdateSettings.fromMap(output).copyWith(
updatePolicy: FirmwareUpdateSettings.firmwareUpdatePolicyAuto),
)
.onError((error, stackTrace) {
// Use default settings on fetch failure
return FirmwareUpdateSettings(
updatePolicy: FirmwareUpdateSettings.firmwareUpdatePolicyAuto,
autoUpdateWindow: FirmwareAutoUpdateWindow(
startMinute: 0,
durationMinutes: 240,
),
);
});

// Enable auto firmware update
try {
await _routerRepository.send(
JNAPAction.setFirmwareUpdateSettings,
fetchRemote: true,
cacheLevel: CacheLevel.noCache,
data: firmwareUpdateSettings.toMap(),
auth: true,
);
} on JNAPError catch (e) {
throw mapJnapErrorToServiceError(e);
}
}

/// Checks internet connection status via JNAP with retry logic.
///
/// Uses [ExponentialBackoffRetryStrategy] with:
/// - maxRetries: 5
/// - initialDelay: 2 seconds
/// - maxDelay: 2 seconds
///
/// JNAP Action: [JNAPAction.getInternetConnectionStatus]
///
/// Returns: [Future<bool>]
/// - `true` if connectionStatus == 'InternetConnected'
/// - `false` if not connected, retries exhausted, or error occurred
///
/// Throws: Nothing (returns false on all error conditions)
Future<bool> checkInternetConnection() async {
// Make up to 5 attempts to check internet connection total 10 seconds
final retryStrategy = ExponentialBackoffRetryStrategy(
maxRetries: 5,
initialDelay: const Duration(seconds: 2),
maxDelay: const Duration(seconds: 2),
);

return retryStrategy.execute<bool>(() async {
final result = await _routerRepository.send(
JNAPAction.getInternetConnectionStatus,
fetchRemote: true,
auth: true,
);
logger.i('[FirstTime]: Internet connection status: ${result.output}');
final connectionStatus = result.output['connectionStatus'];
return connectionStatus == 'InternetConnected';
}, shouldRetry: (result) => !result).onError((error, stackTrace) {
logger.e('[FirstTime]: Error checking internet connection: $error');
return false;
});
}
}
40 changes: 40 additions & 0 deletions lib/page/nodes/models/backhaul_info_ui_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:convert';

import 'package:equatable/equatable.dart';

/// UI-friendly representation of backhaul information
///
/// Replaces BackHaulInfoData (JNAP model) in State/Provider layers.
/// Per constitution Article V Section 5.3.1 - separate models per layer.
class BackhaulInfoUIModel extends Equatable {
final String deviceUUID;
final String connectionType;
final String timestamp;

const BackhaulInfoUIModel({
required this.deviceUUID,
required this.connectionType,
required this.timestamp,
});

@override
List<Object?> get props => [deviceUUID, connectionType, timestamp];

Map<String, dynamic> toMap() => {
'deviceUUID': deviceUUID,
'connectionType': connectionType,
'timestamp': timestamp,
};

factory BackhaulInfoUIModel.fromMap(Map<String, dynamic> map) =>
BackhaulInfoUIModel(
deviceUUID: map['deviceUUID'] ?? '',
connectionType: map['connectionType'] ?? '',
timestamp: map['timestamp'] ?? '',
);

String toJson() => json.encode(toMap());

factory BackhaulInfoUIModel.fromJson(String source) =>
BackhaulInfoUIModel.fromMap(json.decode(source));
}
Loading