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
12 changes: 12 additions & 0 deletions assets/languages/strings_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@
"pdf": "PDF",
"personalData": "Persönliche Daten",
"phoneNumber": "Telefonnummer",
"pinConfirm": "Bestätigen Sie Ihre PIN",
"pinConfirmDescription": "Geben Sie Ihre PIN zur Bestätigung erneut ein",
"pinConfirmFailed": "Die PINs stimmen nicht überein. Versuchen Sie es erneut.",
"pinCreate": "Erstellen Sie Ihre PIN",
"pinCreateDescription": "Geben Sie eine 4-stellige PIN ein, um Ihre Wallet zu sichern",
"pinForgotten": "PIN vergessen? Wallet zurücksetzen",
"pinForgottenDescription": "Durch diese Aktion werden Ihre Wallet und alle zugehörigen Daten gelöscht. Stellen Sie sicher, dass Sie Ihre Wiederherstellungsphrase gesichert haben.",
"pinForgottenTitle": "Wallet wird zurückgesetzt",
"pinVerify": "Geben Sie Ihre PIN ein",
"pinVerifyDescription": "Geben Sie Ihre PIN ein, um Ihre Wallet zu entsperren",
"pinVerifyFailed": "Die PIN ist falsch. Versuchen Sie es erneut.",
"pleaseSelect": "Bitte auswählen",
"portfolio": "Bestand",
"postcodeAbr": "PLZ",
Expand Down Expand Up @@ -137,6 +148,7 @@
"registerPhoneNumberTooShort": "Telefonnummer ist zu kurz",
"registrationRequired": "Registrierung erforderlich",
"registrationRequiredDescription": "Um REALU zu kaufen, müssen Sie Ihre Wallet registrieren. Dabei entsteht eine Geschäftsbeziehung mit RealUnit AG und DFX AG.",
"reset": "Zurücksetzen",
"residence": "Residenz",
"restoreWallet": "Wallet wiederherstellen",
"restoreWalletFromSeed": "Wallet mit Seed wiederherstellen",
Expand Down
12 changes: 12 additions & 0 deletions assets/languages/strings_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@
"pdf": "PDF",
"personalData": "Personal data",
"phoneNumber": "Phone number",
"pinConfirm": "Confirm your pin",
"pinConfirmDescription": "Re-enter your PIN to confirm",
"pinConfirmFailed": "PINs do not match. Try again.",
"pinCreate": "Create your pin",
"pinCreateDescription": "Enter a 4-digit PIN to secure your wallet",
"pinForgotten": "Forgot PIN? Reset Wallet",
"pinForgottenDescription": "This action will delete your wallet and all associated data. Make sure you have backed up your recovery phrase.",
"pinForgottenTitle": "Wallet will be reset",
"pinVerify": "Enter your pin",
"pinVerifyDescription": "Enter your PIN to unlock your wallet",
"pinVerifyFailed": "PIN is wrong. Try again.",
"pleaseSelect": "Please select",
"portfolio": "Portfolio",
"postcodeAbr": "Post code",
Expand Down Expand Up @@ -137,6 +148,7 @@
"registerPhoneNumberTooShort": "Phone number is too short",
"registrationRequired": "Registration required",
"registrationRequiredDescription": "To purchase REALU, you must register your wallet. This establishes a business relationship with RealUnit AG and DFX AG.",
"reset": "Reset",
"residence": "Residence",
"restoreWallet": "Restore wallet",
"restoreWalletFromSeed": "Restore wallet with seed",
Expand Down
4 changes: 4 additions & 0 deletions lib/di.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'package:realunit_wallet/packages/storage/database.dart';
import 'package:realunit_wallet/packages/storage/secure_storage.dart';
import 'package:realunit_wallet/router.dart';
import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart';
import 'package:realunit_wallet/screens/pin/bloc/auth/pin_auth_cubit.dart';
import 'package:realunit_wallet/screens/settings/bloc/settings_bloc.dart';
import 'package:realunit_wallet/setup.dart';
import 'package:shared_preferences/shared_preferences.dart';
Expand Down Expand Up @@ -152,6 +153,9 @@ void setupBlocs() {
getIt<AppStore>(),
),
);
getIt.registerSingleton(
PinAuthCubit(getIt<SecureStorage>(), getIt<SettingsRepository>())..initialize(),
);
}

Future<bool> _existsDatabaseFile() async => File(await AppDatabase.getDatabasePath()).exists();
Expand Down
103 changes: 67 additions & 36 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/packages/service/app_store.dart';
import 'package:realunit_wallet/packages/service/balance_service.dart';
import 'package:realunit_wallet/packages/utils/fuck_firebase.dart';
import 'package:realunit_wallet/screens/dashboard/dashboard_page.dart';
import 'package:realunit_wallet/screens/home/bloc/home_bloc.dart';
import 'package:realunit_wallet/screens/onboarding/onboarding_completed_page.dart';
import 'package:realunit_wallet/screens/pin/bloc/auth/pin_auth_cubit.dart';
import 'package:realunit_wallet/screens/pin/setup_pin_page.dart';
import 'package:realunit_wallet/screens/pin/verify_pin_page.dart';
import 'package:realunit_wallet/screens/settings/bloc/settings_bloc.dart';
import 'package:realunit_wallet/styles/themes.dart';

Expand Down Expand Up @@ -76,59 +80,86 @@ class _WalletAppState extends State<WalletApp> {
case AppLifecycleState.paused:
_onPaused();
}
developer.log(state.name, name: 'AppLifecycleListener');
}

void _onDetached() => developer.log('detached');
void _onDetached() {}

void _onResumed() {
getIt<BalanceService>()
.updateBalances(getIt<AppStore>().primaryAddress);
getIt<PinAuthCubit>().onAppResumed();
getIt<BalanceService>().updateBalances(getIt<AppStore>().primaryAddress);
}

void _onInactive() => developer.log('inactive', name: 'AppLifecycleListener');
void _onInactive() {}

void _onHidden() => developer.log('hidden', name: 'AppLifecycleListener');
void _onHidden() {
getIt<PinAuthCubit>().onAppHidden();
}

void _onPaused() {
getIt<BalanceService>().cancelSync();
}

@override
Widget build(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider.value(value: getIt<HomeBloc>()),
BlocProvider.value(value: getIt<SettingsBloc>()),
providers: [
BlocProvider.value(value: getIt<HomeBloc>()),
BlocProvider.value(value: getIt<SettingsBloc>()),
BlocProvider.value(value: getIt<PinAuthCubit>()),
],
child: BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, settingsState) => MaterialApp.router(
debugShowCheckedModeBanner: false,
theme: realUnitTheme,
supportedLocales: S.delegate.supportedLocales,
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
child: BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) => MaterialApp.router(
debugShowCheckedModeBanner: false,
theme: realUnitTheme,
supportedLocales: S.delegate.supportedLocales,
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
locale: Locale(state.language.code),
routerConfig: getIt<GoRouter>(),
builder: (context, child) => BlocListener<HomeBloc, HomeState>(
listener: (context, state) {
if (!state.isLoadingWallet) {
var targetRoute = '/welcome';
if (state.openWallet != null) {
targetRoute = OnboardingCompletedPage.route;
if (state.onboardingCompleted) {
targetRoute = '/dashboard';
}
}

getIt<GoRouter>().go(targetRoute);
locale: Locale(settingsState.language.code),
routerConfig: getIt<GoRouter>(),
builder: (context, child) => MultiBlocListener(
listeners: [
BlocListener<HomeBloc, HomeState>(
listener: (context, homeState) {
if (!homeState.isLoadingWallet) {
_navigate(context);
}
},
child: child,
),
),
BlocListener<PinAuthCubit, PinAuthState>(
listener: (context, pinState) {
_navigate(context);
},
),
],
child: child ?? const SizedBox.shrink(),
),
);
),
),
);

void _navigate(BuildContext context) {
final homeState = context.read<HomeBloc>().state;
final pinState = context.read<PinAuthCubit>().state;

if (homeState.isLoadingWallet) return;

String targetRoute;
if (homeState.openWallet == null) {
targetRoute = '/welcome';
} else if (!homeState.onboardingCompleted) {
targetRoute = OnboardingCompletedPage.route;
} else if (!pinState.isPinSetup) {
targetRoute = SetupPinPage.route;
} else if (!pinState.isPinVerified) {
targetRoute = VerifyPinPage.route;
} else {
targetRoute = DashboardPage.routeName;
}

getIt<GoRouter>().go(targetRoute);
}
}
4 changes: 4 additions & 0 deletions lib/packages/repository/settings_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ class SettingsRepository {
}

set networkMode(NetworkMode mode) => _sharedPreferences.setString('networkMode', mode.name);

bool get isPinEnabled => _sharedPreferences.getBool('isPinEnabled') ?? false;

set isPinEnabled(bool enabled) => _sharedPreferences.setBool('isPinEnabled', enabled);
}
19 changes: 19 additions & 0 deletions lib/packages/storage/secure_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import 'package:web3dart/crypto.dart';

class SecureStorage {
static const _encryptionKey = 'drift.encryption.password';
static const _pinHashKey = 'pin.hash';

final FlutterSecureStorage _secureStorage;

const SecureStorage() : _secureStorage = const FlutterSecureStorage();

// Database

static String getNewEncryptionKey({int keySize = 32, int iterations = 10000}) {
final key = const Uuid().v4();
final salt = Uint8List(9)..setRange(0, 9, utf8.encode('dEURO key'));
Expand All @@ -28,4 +31,20 @@ class SecureStorage {

Future<void> setEncryptionKey(String key) =>
_secureStorage.write(key: _encryptionKey, value: key);

// Pin

static String hashPin(String pin) {
final salt = Uint8List(8)..setRange(0, 8, utf8.encode('PIN salt'));
final derivator = KeyDerivator('SHA-256/HMAC/PBKDF2');
final params = Pbkdf2Parameters(salt, 10000, 32);
derivator.init(params);
return bytesToHex(derivator.process(utf8.encode(pin)));
}

Future<String?> getPinHash() => _secureStorage.read(key: _pinHashKey);

Future<void> setPinHash(String hash) => _secureStorage.write(key: _pinHashKey, value: hash);

Future<void> deletePinHash() => _secureStorage.delete(key: _pinHashKey);
}
10 changes: 10 additions & 0 deletions lib/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'package:realunit_wallet/screens/dashboard/dashboard_page.dart';
import 'package:realunit_wallet/screens/home/home.dart';
import 'package:realunit_wallet/screens/kyc/kyc_page_manager.dart';
import 'package:realunit_wallet/screens/onboarding/onboarding_completed_page.dart';
import 'package:realunit_wallet/screens/pin/setup_pin_page.dart';
import 'package:realunit_wallet/screens/pin/verify_pin_page.dart';
import 'package:realunit_wallet/screens/receive/receive_page.dart';
import 'package:realunit_wallet/screens/restore_wallet/restore_wallet_page.dart';
import 'package:realunit_wallet/screens/sell/sell_page.dart';
Expand Down Expand Up @@ -58,6 +60,14 @@ void setupRouter() {
path: OnboardingCompletedPage.route,
builder: (context, state) => const OnboardingCompletedPage(),
),
GoRoute(
path: SetupPinPage.route,
builder: (context, state) => const SetupPinPage(),
),
GoRoute(
path: VerifyPinPage.route,
builder: (context, state) => const VerifyPinPage(),
),
GoRoute(
path: DashboardPage.routeName,
builder: (context, state) => const DashboardPage(),
Expand Down
57 changes: 57 additions & 0 deletions lib/screens/pin/bloc/auth/pin_auth_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:realunit_wallet/packages/repository/settings_repository.dart';
import 'package:realunit_wallet/packages/storage/secure_storage.dart';

part 'pin_auth_state.dart';

class PinAuthCubit extends Cubit<PinAuthState> {
static const _lockoutDuration = Duration(minutes: 1);

PinAuthCubit(
this._secureStorage,
this._settingsRepository,
) : super(const PinAuthState());

final SecureStorage _secureStorage;
final SettingsRepository _settingsRepository;

DateTime? _lastBackgroundTime;

void initialize() {
final isPinSetup = _settingsRepository.isPinEnabled;
emit(
state.copyWith(
isPinSetup: isPinSetup,
isPinVerified: !isPinSetup,
),
);
}

void onPinSetupComplete() => emit(
state.copyWith(isPinSetup: true, isPinVerified: true),
);

void onPinVerified() => emit(state.copyWith(isPinVerified: true));

void onAppHidden() => _lastBackgroundTime ??= DateTime.now();

void onAppResumed() {
if (!state.isPinSetup) return;

final lastBackground = _lastBackgroundTime;
if (lastBackground == null) return;

final elapsed = DateTime.now().difference(lastBackground);
if (elapsed >= _lockoutDuration) {
emit(state.copyWith(isPinVerified: false));
}
_lastBackgroundTime = null;
}

void reset() {
_settingsRepository.isPinEnabled = false;
_secureStorage.deletePinHash();
emit(const PinAuthState());
}
}
24 changes: 24 additions & 0 deletions lib/screens/pin/bloc/auth/pin_auth_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
part of 'pin_auth_cubit.dart';

class PinAuthState extends Equatable {
final bool isPinSetup;
final bool isPinVerified;

const PinAuthState({
this.isPinSetup = false,
this.isPinVerified = false,
});

PinAuthState copyWith({
bool? isPinSetup,
bool? isPinVerified,
}) {
return PinAuthState(
isPinSetup: isPinSetup ?? this.isPinSetup,
isPinVerified: isPinVerified ?? this.isPinVerified,
);
}

@override
List<Object?> get props => [isPinSetup, isPinVerified];
}
41 changes: 0 additions & 41 deletions lib/screens/pin/bloc/pin_cubit.dart

This file was deleted.

Loading