mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 03:02:12 +00:00
starting with a proper setup
This commit is contained in:
parent
8021768883
commit
c47c91c1ba
28 changed files with 979 additions and 206 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
- New: Feature to find friends without a phone number
|
- New: Feature to find friends without a phone number
|
||||||
- New: The verification state is now transferred to the scanned user.
|
- New: The verification state is now transferred to the scanned user.
|
||||||
|
- New: Registration setup to configure the most important configurations
|
||||||
- Improved: FAQ is now in the app rather than opening in the browser
|
- Improved: FAQ is now in the app rather than opening in the browser
|
||||||
- Fix: Many smaller issues
|
- Fix: Many smaller issues
|
||||||
|
|
||||||
|
|
|
||||||
16
lib/app.dart
16
lib/app.dart
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
@ -18,7 +17,7 @@ import 'package:twonly/src/visual/views/critical_error.view.dart';
|
||||||
import 'package:twonly/src/visual/views/home.view.dart';
|
import 'package:twonly/src/visual/views/home.view.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
|
import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/register.view.dart';
|
import 'package:twonly/src/visual/views/onboarding/register.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
|
import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
|
||||||
|
|
||||||
class App extends StatefulWidget {
|
class App extends StatefulWidget {
|
||||||
|
|
@ -119,7 +118,6 @@ class AppMainWidget extends StatefulWidget {
|
||||||
class _AppMainWidgetState extends State<AppMainWidget> {
|
class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
bool _showOnboarding = true;
|
bool _showOnboarding = true;
|
||||||
bool _isLoaded = false;
|
bool _isLoaded = false;
|
||||||
bool _skipBackup = kDebugMode;
|
|
||||||
bool _isTwonlyLocked = true;
|
bool _isTwonlyLocked = true;
|
||||||
|
|
||||||
(Future<int>?, bool) _proofOfWork = (null, false);
|
(Future<int>?, bool) _proofOfWork = (null, false);
|
||||||
|
|
@ -171,11 +169,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
_isTwonlyLocked = false;
|
_isTwonlyLocked = false;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if (userService.currentUser.twonlySafeBackup == null &&
|
} else if (true ||
|
||||||
!_skipBackup) {
|
!userService.currentUser.skipSetupPages &&
|
||||||
child = SetupBackupView(
|
userService.currentUser.currentSetupPage ==
|
||||||
callBack: () => setState(() {
|
SetupPages.profile.name) {
|
||||||
_skipBackup = true;
|
child = SetupView(
|
||||||
|
onUpdate: () => setState(() {
|
||||||
|
// userService.currentUser has updated...
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ class AppState {
|
||||||
static bool isInBackgroundTask = false;
|
static bool isInBackgroundTask = false;
|
||||||
static bool allowErrorTrackingViaSentry = false;
|
static bool allowErrorTrackingViaSentry = false;
|
||||||
static bool gotMessageFromServer = false;
|
static bool gotMessageFromServer = false;
|
||||||
|
// initialized in runMigrations (main.dart)
|
||||||
|
static late int latestAppVersionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppGlobalKeys {
|
class AppGlobalKeys {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/avatars.dart';
|
import 'package:twonly/src/utils/avatars.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/secure_storage.dart';
|
import 'package:twonly/src/utils/secure_storage.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
|
||||||
/// This function is used to initialized the absolute minimum so it
|
/// This function is used to initialized the absolute minimum so it
|
||||||
/// can also be used by the backend without the UI was loaded.
|
/// can also be used by the backend without the UI was loaded.
|
||||||
|
|
@ -147,6 +148,13 @@ Future<void> runMigrations() async {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await UserService.update((u) => u.appVersion = 109);
|
await UserService.update((u) {
|
||||||
|
u
|
||||||
|
..appVersion = 109
|
||||||
|
..skipSetupPages = true
|
||||||
|
..currentSetupPage = SetupPages.userDiscovery.name;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppState.latestAppVersionId = 110;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1126,6 +1126,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Enable'**
|
/// **'Enable'**
|
||||||
String get enable;
|
String get enable;
|
||||||
|
|
||||||
|
/// No description provided for @understood.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Understood'**
|
||||||
|
String get understood;
|
||||||
|
|
||||||
/// No description provided for @cancel.
|
/// No description provided for @cancel.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2878,6 +2884,54 @@ abstract class AppLocalizations {
|
||||||
/// **'Skip for now'**
|
/// **'Skip for now'**
|
||||||
String get skipForNow;
|
String get skipForNow;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingFinishLater.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Finish later'**
|
||||||
|
String get onboardingFinishLater;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose your look'**
|
||||||
|
String get onboardingProfileTitle;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingProfileBody.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Select an avatar and a display name that friends will see.'**
|
||||||
|
String get onboardingProfileBody;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingBackupTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Backup Setup'**
|
||||||
|
String get onboardingBackupTitle;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingBackupBody.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'**
|
||||||
|
String get onboardingBackupBody;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingVerificationBadgeTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Verification Badge'**
|
||||||
|
String get onboardingVerificationBadgeTitle;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingUserDiscoveryTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'User Discovery'**
|
||||||
|
String get onboardingUserDiscoveryTitle;
|
||||||
|
|
||||||
|
/// No description provided for @onboardingResetSetup.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reset Setup'**
|
||||||
|
String get onboardingResetSetup;
|
||||||
|
|
||||||
/// No description provided for @linkFromUsername.
|
/// No description provided for @linkFromUsername.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3079,19 +3133,19 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @verificationBadgeGreenDesc.
|
/// No description provided for @verificationBadgeGreenDesc.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'A contact you have personally verified.'**
|
/// **'A contact you have *personally* verified.'**
|
||||||
String get verificationBadgeGreenDesc;
|
String get verificationBadgeGreenDesc;
|
||||||
|
|
||||||
/// No description provided for @verificationBadgeYellowDesc.
|
/// No description provided for @verificationBadgeYellowDesc.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'A contact who has been verified by at least one of your contacts.'**
|
/// **'A contact who has been verified by at least one of *your contacts*.'**
|
||||||
String get verificationBadgeYellowDesc;
|
String get verificationBadgeYellowDesc;
|
||||||
|
|
||||||
/// No description provided for @verificationBadgeRedDesc.
|
/// No description provided for @verificationBadgeRedDesc.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'A contact whose identity has not yet been verified.'**
|
/// **'A contact whose identity has *not* yet been verified.'**
|
||||||
String get verificationBadgeRedDesc;
|
String get verificationBadgeRedDesc;
|
||||||
|
|
||||||
/// No description provided for @chatEntryFlameRestored.
|
/// No description provided for @chatEntryFlameRestored.
|
||||||
|
|
|
||||||
|
|
@ -575,6 +575,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get enable => 'Aktivieren';
|
String get enable => 'Aktivieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get understood => 'Verstanden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancel => 'Abbrechen';
|
String get cancel => 'Abbrechen';
|
||||||
|
|
||||||
|
|
@ -1588,6 +1591,32 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get skipForNow => 'Vorerst überspringen';
|
String get skipForNow => 'Vorerst überspringen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingFinishLater => 'Später abschließen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileTitle => 'Wähle deinen Look';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileBody =>
|
||||||
|
'Wähle einen Avatar und einen Anzeigenamen, den deine Freunde sehen werden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingBackupTitle => 'Backup einrichten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingBackupBody =>
|
||||||
|
'Sichere deine twonly-Identität, da dies die einzige Möglichkeit ist, dein Konto wiederherzustellen, wenn du die App deinstallierst oder dein Handy verlierst.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingVerificationBadgeTitle => 'Verifizierungs-Abzeichen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingUserDiscoveryTitle => 'Freunde finden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingResetSetup => 'Setup zurücksetzen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String linkFromUsername(Object username) {
|
String linkFromUsername(Object username) {
|
||||||
return 'Ist der Link von $username?';
|
return 'Ist der Link von $username?';
|
||||||
|
|
@ -1721,15 +1750,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGreenDesc =>
|
String get verificationBadgeGreenDesc =>
|
||||||
'Ein Kontakt, den du persönlich verifiziert hast.';
|
'Ein Kontakt, den du *persönlich verifiziert* hast.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeYellowDesc =>
|
String get verificationBadgeYellowDesc =>
|
||||||
'Ein Kontakt, der von mind. einem deiner Kontakte verifiziert wurde.';
|
'Ein Kontakt, der von mind. einem *deiner Kontakte verifiziert* wurde.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeRedDesc =>
|
String get verificationBadgeRedDesc =>
|
||||||
'Ein Kontakt, dessen Identität noch nicht überprüft wurde.';
|
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String chatEntryFlameRestored(Object count) {
|
String chatEntryFlameRestored(Object count) {
|
||||||
|
|
|
||||||
|
|
@ -570,6 +570,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get enable => 'Enable';
|
String get enable => 'Enable';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get understood => 'Understood';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancel => 'Cancel';
|
String get cancel => 'Cancel';
|
||||||
|
|
||||||
|
|
@ -1578,6 +1581,32 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get skipForNow => 'Skip for now';
|
String get skipForNow => 'Skip for now';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingFinishLater => 'Finish later';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileTitle => 'Choose your look';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileBody =>
|
||||||
|
'Select an avatar and a display name that friends will see.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingBackupTitle => 'Backup Setup';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingBackupBody =>
|
||||||
|
'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingVerificationBadgeTitle => 'Verification Badge';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingUserDiscoveryTitle => 'User Discovery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingResetSetup => 'Reset Setup';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String linkFromUsername(Object username) {
|
String linkFromUsername(Object username) {
|
||||||
return 'Is the link from $username?';
|
return 'Is the link from $username?';
|
||||||
|
|
@ -1709,15 +1738,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGreenDesc =>
|
String get verificationBadgeGreenDesc =>
|
||||||
'A contact you have personally verified.';
|
'A contact you have *personally* verified.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeYellowDesc =>
|
String get verificationBadgeYellowDesc =>
|
||||||
'A contact who has been verified by at least one of your contacts.';
|
'A contact who has been verified by at least one of *your contacts*.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeRedDesc =>
|
String get verificationBadgeRedDesc =>
|
||||||
'A contact whose identity has not yet been verified.';
|
'A contact whose identity has *not* yet been verified.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String chatEntryFlameRestored(Object count) {
|
String chatEntryFlameRestored(Object count) {
|
||||||
|
|
|
||||||
|
|
@ -570,6 +570,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get enable => 'Enable';
|
String get enable => 'Enable';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get understood => 'Understood';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancel => 'Cancel';
|
String get cancel => 'Cancel';
|
||||||
|
|
||||||
|
|
@ -1578,6 +1581,32 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get skipForNow => 'Skip for now';
|
String get skipForNow => 'Skip for now';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingFinishLater => 'Finish later';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileTitle => 'Choose your look';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingProfileBody =>
|
||||||
|
'Select an avatar and a display name that friends will see.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingBackupTitle => 'Backup Setup';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingBackupBody =>
|
||||||
|
'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingVerificationBadgeTitle => 'Verification Badge';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingUserDiscoveryTitle => 'User Discovery';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onboardingResetSetup => 'Reset Setup';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String linkFromUsername(Object username) {
|
String linkFromUsername(Object username) {
|
||||||
return 'Is the link from $username?';
|
return 'Is the link from $username?';
|
||||||
|
|
@ -1709,15 +1738,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGreenDesc =>
|
String get verificationBadgeGreenDesc =>
|
||||||
'A contact you have personally verified.';
|
'A contact you have *personally* verified.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeYellowDesc =>
|
String get verificationBadgeYellowDesc =>
|
||||||
'A contact who has been verified by at least one of your contacts.';
|
'A contact who has been verified by at least one of *your contacts*.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeRedDesc =>
|
String get verificationBadgeRedDesc =>
|
||||||
'A contact whose identity has not yet been verified.';
|
'A contact whose identity has *not* yet been verified.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String chatEntryFlameRestored(Object count) {
|
String chatEntryFlameRestored(Object count) {
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit e50fdde3db3a81f2db03a03e7f64d6351cc507a4
|
Subproject commit 82c248ae18ffda0bf161fbb69ddb3fb30cfc1531
|
||||||
|
|
@ -9,6 +9,7 @@ class UserData {
|
||||||
required this.username,
|
required this.username,
|
||||||
required this.displayName,
|
required this.displayName,
|
||||||
required this.subscriptionPlan,
|
required this.subscriptionPlan,
|
||||||
|
required this.currentSetupPage,
|
||||||
});
|
});
|
||||||
factory UserData.fromJson(Map<String, dynamic> json) =>
|
factory UserData.fromJson(Map<String, dynamic> json) =>
|
||||||
_$UserDataFromJson(json);
|
_$UserDataFromJson(json);
|
||||||
|
|
@ -136,6 +137,11 @@ class UserData {
|
||||||
// Once a day the anonymous data is collected and send to the server
|
// Once a day the anonymous data is collected and send to the server
|
||||||
DateTime? lastUserStudyDataUpload;
|
DateTime? lastUserStudyDataUpload;
|
||||||
|
|
||||||
|
String? currentSetupPage;
|
||||||
|
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
bool skipSetupPages = false;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$UserDataToJson(this);
|
Map<String, dynamic> toJson() => _$UserDataToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
||||||
username: json['username'] as String,
|
username: json['username'] as String,
|
||||||
displayName: json['displayName'] as String,
|
displayName: json['displayName'] as String,
|
||||||
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
|
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
|
||||||
|
currentSetupPage: json['currentSetupPage'] as String?,
|
||||||
)
|
)
|
||||||
..avatarSvg = json['avatarSvg'] as String?
|
..avatarSvg = json['avatarSvg'] as String?
|
||||||
..avatarJson = json['avatarJson'] as String?
|
..avatarJson = json['avatarJson'] as String?
|
||||||
|
|
@ -93,7 +94,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
||||||
json['userStudyParticipantsToken'] as String?
|
json['userStudyParticipantsToken'] as String?
|
||||||
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
|
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['lastUserStudyDataUpload'] as String);
|
: DateTime.parse(json['lastUserStudyDataUpload'] as String)
|
||||||
|
..skipSetupPages = json['skipSetupPages'] as bool? ?? false;
|
||||||
|
|
||||||
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'userId': instance.userId,
|
'userId': instance.userId,
|
||||||
|
|
@ -145,6 +147,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'userStudyParticipantsToken': instance.userStudyParticipantsToken,
|
'userStudyParticipantsToken': instance.userStudyParticipantsToken,
|
||||||
'lastUserStudyDataUpload': instance.lastUserStudyDataUpload
|
'lastUserStudyDataUpload': instance.lastUserStudyDataUpload
|
||||||
?.toIso8601String(),
|
?.toIso8601String(),
|
||||||
|
'currentSetupPage': instance.currentSetupPage,
|
||||||
|
'skipSetupPages': instance.skipSetupPages,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$ThemeModeEnumMap = {
|
const _$ThemeModeEnumMap = {
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import 'package:twonly/src/visual/views/settings/help/contact_us.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/credits.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/credits.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/diagnostics.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/diagnostics.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/faq/verification_bade_faq.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/faq/verification_badge_faq.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/help.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/help.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/notification.view.dart';
|
import 'package:twonly/src/visual/views/settings/notification.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/privacy.view.dart';
|
import 'package:twonly/src/visual/views/settings/privacy.view.dart';
|
||||||
|
|
|
||||||
|
|
@ -290,9 +290,11 @@ Future<List<int>> sha256File(File file) async {
|
||||||
return sha256Sink.events.single.bytes;
|
return sha256Sink.events.single.bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TextSpan> formattedText(BuildContext context, String input) {
|
List<TextSpan> formattedText(
|
||||||
// Access the current theme's text color
|
BuildContext context,
|
||||||
// Defaulting to bodyMedium color, but you can use labelLarge, displaySmall, etc.
|
String input, {
|
||||||
|
Color? boldTextColor,
|
||||||
|
}) {
|
||||||
final defaultColor = Theme.of(context).colorScheme.onSurface;
|
final defaultColor = Theme.of(context).colorScheme.onSurface;
|
||||||
|
|
||||||
final regex = RegExp(r'\*(.*?)\*');
|
final regex = RegExp(r'\*(.*?)\*');
|
||||||
|
|
@ -301,7 +303,6 @@ List<TextSpan> formattedText(BuildContext context, String input) {
|
||||||
var lastMatchEnd = 0;
|
var lastMatchEnd = 0;
|
||||||
|
|
||||||
for (final match in regex.allMatches(input)) {
|
for (final match in regex.allMatches(input)) {
|
||||||
// Add text before the match (Normal style)
|
|
||||||
if (match.start > lastMatchEnd) {
|
if (match.start > lastMatchEnd) {
|
||||||
spans.add(
|
spans.add(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
|
|
@ -311,13 +312,12 @@ List<TextSpan> formattedText(BuildContext context, String input) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the matched text (Bold style)
|
|
||||||
spans.add(
|
spans.add(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: match.group(1),
|
text: match.group(1),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: defaultColor, // Ensures bold text also uses the theme color
|
color: boldTextColor ?? defaultColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -325,7 +325,6 @@ List<TextSpan> formattedText(BuildContext context, String input) {
|
||||||
lastMatchEnd = match.end;
|
lastMatchEnd = match.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add any remaining text after the last match
|
|
||||||
if (lastMatchEnd < input.length) {
|
if (lastMatchEnd < input.length) {
|
||||||
spans.add(
|
spans.add(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/faq/verification_bade_faq.view.dart';
|
|
||||||
|
|
||||||
class VerificationBadgeComp extends StatefulWidget {
|
class VerificationBadgeComp extends StatefulWidget {
|
||||||
const VerificationBadgeComp({
|
const VerificationBadgeComp({
|
||||||
|
|
|
||||||
74
lib/src/visual/components/verification_badge_info.comp.dart
Normal file
74
lib/src/visual/components/verification_badge_info.comp.dart
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
||||||
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
||||||
|
const colorVerificationBadgeYellow = Color.fromARGB(255, 0, 182, 238);
|
||||||
|
|
||||||
|
class VerificationBadgeInfo extends StatelessWidget {
|
||||||
|
const VerificationBadgeInfo({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.lang.verificationBadgeGeneralDesc,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
_buildItem(
|
||||||
|
context,
|
||||||
|
icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40),
|
||||||
|
description: context.lang.verificationBadgeGreenDesc,
|
||||||
|
boldTextColor: primaryColor,
|
||||||
|
),
|
||||||
|
_buildItem(
|
||||||
|
context,
|
||||||
|
icon: const SvgIcon(
|
||||||
|
assetPath: SvgIcons.verifiedGreen,
|
||||||
|
size: 40,
|
||||||
|
color: colorVerificationBadgeYellow,
|
||||||
|
),
|
||||||
|
description: context.lang.verificationBadgeYellowDesc,
|
||||||
|
boldTextColor: colorVerificationBadgeYellow,
|
||||||
|
),
|
||||||
|
_buildItem(
|
||||||
|
context,
|
||||||
|
icon: const SvgIcon(assetPath: SvgIcons.verifiedRed, size: 40),
|
||||||
|
description: context.lang.verificationBadgeRedDesc,
|
||||||
|
boldTextColor: const Color(0xffff0000),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItem(
|
||||||
|
BuildContext context, {
|
||||||
|
required Widget icon,
|
||||||
|
required String description,
|
||||||
|
required Color boldTextColor,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 25),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
icon,
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(
|
||||||
|
child: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: formattedText(
|
||||||
|
context,
|
||||||
|
description,
|
||||||
|
boldTextColor: boldTextColor,
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
|
|
@ -19,6 +20,7 @@ import 'package:twonly/src/utils/secure_storage.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
import 'package:twonly/src/visual/views/groups/group.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
|
||||||
class RegisterView extends StatefulWidget {
|
class RegisterView extends StatefulWidget {
|
||||||
const RegisterView({
|
const RegisterView({
|
||||||
|
|
@ -136,7 +138,8 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
username: username,
|
username: username,
|
||||||
displayName: username,
|
displayName: username,
|
||||||
subscriptionPlan: 'Preview',
|
subscriptionPlan: 'Preview',
|
||||||
)..appVersion = 62;
|
currentSetupPage: SetupPages.profile.name,
|
||||||
|
)..appVersion = AppState.latestAppVersionId;
|
||||||
|
|
||||||
await SecureStorage.instance.write(
|
await SecureStorage.instance.write(
|
||||||
key: SecureStorageKeys.userData,
|
key: SecureStorageKeys.userData,
|
||||||
|
|
|
||||||
130
lib/src/visual/views/onboarding/setup.view.dart
Normal file
130
lib/src/visual/views/onboarding/setup.view.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/setup/backup_setup.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/setup/profile_setup.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/setup/user_discovery_setup.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/setup/verification_badge_setup.view.dart';
|
||||||
|
|
||||||
|
enum SetupPages {
|
||||||
|
profile,
|
||||||
|
backup,
|
||||||
|
verificationBadge,
|
||||||
|
userDiscovery,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SetupPagesExtension on SetupPages {
|
||||||
|
int get pageNumber => index + 1;
|
||||||
|
int get totalPages => SetupPages.values.length;
|
||||||
|
int get progressPercentage => (pageNumber / totalPages * 100).round();
|
||||||
|
String get progressText => '$pageNumber / $totalPages';
|
||||||
|
|
||||||
|
SetupPages? next() {
|
||||||
|
final nextIndex = index + 1;
|
||||||
|
if (nextIndex < SetupPages.values.length) {
|
||||||
|
return SetupPages.values[nextIndex];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SetupView extends StatefulWidget {
|
||||||
|
const SetupView({this.onUpdate, super.key});
|
||||||
|
|
||||||
|
final VoidCallback? onUpdate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SetupView> createState() => _SetupViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SetupViewState extends State<SetupView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder<void>(
|
||||||
|
stream: userService.onUserUpdated,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final user = userService.currentUser;
|
||||||
|
final currentPageString = user.currentSetupPage;
|
||||||
|
|
||||||
|
final currentPage = SetupPages.values.firstWhere(
|
||||||
|
(e) => e.name == currentPageString,
|
||||||
|
orElse: () => SetupPages.profile,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: List.generate(currentPage.totalPages, (index) {
|
||||||
|
final isFinished = index < currentPage.pageNumber;
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 6,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
right: index == currentPage.totalPages - 1 ? 0 : 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isFinished
|
||||||
|
? context.color.primary
|
||||||
|
: context.color.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
toolbarHeight: 48,
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 12),
|
||||||
|
children: [
|
||||||
|
_buildPage(currentPage),
|
||||||
|
SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await UserService.update((u) {
|
||||||
|
u
|
||||||
|
..skipSetupPages = false
|
||||||
|
..currentSetupPage = SetupPages.profile.name;
|
||||||
|
});
|
||||||
|
//await UserService.update((u) => u.skipSetupPages = true);
|
||||||
|
widget.onUpdate?.call();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
context.lang.onboardingFinishLater,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPage(SetupPages page) {
|
||||||
|
switch (page) {
|
||||||
|
case SetupPages.profile:
|
||||||
|
return const ProfileSetupPage();
|
||||||
|
case SetupPages.backup:
|
||||||
|
return const BackupSetupPage();
|
||||||
|
case SetupPages.verificationBadge:
|
||||||
|
return const VerificationBadgeSetupPage();
|
||||||
|
case SetupPages.userDiscovery:
|
||||||
|
return const UserDiscoverySetupPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
172
lib/src/visual/views/onboarding/setup/backup_setup.view.dart
Normal file
172
lib/src/visual/views/onboarding/setup/backup_setup.view.dart
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/services/backup/common.backup.dart';
|
||||||
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/settings/backup/components/backup_setup.comp.dart';
|
||||||
|
|
||||||
|
class BackupSetupPage extends StatefulWidget {
|
||||||
|
const BackupSetupPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BackupSetupPage> createState() => _BackupSetupPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackupSetupPageState extends State<BackupSetupPage> {
|
||||||
|
bool isLoading = false;
|
||||||
|
final TextEditingController passwordCtrl = TextEditingController();
|
||||||
|
final TextEditingController repeatedPasswordCtrl = TextEditingController();
|
||||||
|
|
||||||
|
Future<void> onPressedEnableTwonlySafe() async {
|
||||||
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!await isSecurePassword(passwordCtrl.text)) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final ignore = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.backupInsecurePassword,
|
||||||
|
context.lang.backupInsecurePasswordDesc,
|
||||||
|
customCancel: context.lang.backupInsecurePasswordOk,
|
||||||
|
customOk: context.lang.backupInsecurePasswordCancel,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (ignore) {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
await enableTwonlySafe(passwordCtrl.text);
|
||||||
|
|
||||||
|
await UserService.update((user) {
|
||||||
|
user.currentSetupPage = SetupPages.backup.next()?.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
passwordCtrl.dispose();
|
||||||
|
repeatedPasswordCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isPasswordValid = passwordCtrl.text.length >= 10;
|
||||||
|
final isRepeatedPasswordValid =
|
||||||
|
passwordCtrl.text == repeatedPasswordCtrl.text;
|
||||||
|
final canSubmit =
|
||||||
|
!isLoading &&
|
||||||
|
(isPasswordValid && isRepeatedPasswordValid || !kReleaseMode);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
'twonly Backup',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
context.lang.onboardingBackupBody,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
BackupPasswordTextField(
|
||||||
|
controller: passwordCtrl,
|
||||||
|
labelText: context.lang.password,
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
PasswordRequirementText(
|
||||||
|
text: context.lang.backupPasswordRequirement,
|
||||||
|
showError: passwordCtrl.text.isNotEmpty && !isPasswordValid,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
BackupPasswordTextField(
|
||||||
|
controller: repeatedPasswordCtrl,
|
||||||
|
labelText: context.lang.passwordRepeated,
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
PasswordRequirementText(
|
||||||
|
text: context.lang.passwordRepeatedNotEqual,
|
||||||
|
showError:
|
||||||
|
repeatedPasswordCtrl.text.isNotEmpty && !isRepeatedPasswordValid,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const FaIcon(
|
||||||
|
FontAwesomeIcons.circleInfo,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
context.lang.backupNoPasswordRecovery,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => context.push(Routes.settingsBackupServer),
|
||||||
|
child: Text(context.lang.backupExpertSettings),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: canSubmit ? onPressedEnableTwonlySafe : null,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
backgroundColor: context.color.primary,
|
||||||
|
foregroundColor: context.color.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
context.lang.next,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/src/visual/views/onboarding/setup/finish_setup.comp.dart
Normal file
15
lib/src/visual/views/onboarding/setup/finish_setup.comp.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FinishSetupComp extends StatefulWidget {
|
||||||
|
const FinishSetupComp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FinishSetupComp> createState() => _FinishSetupCompState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FinishSetupCompState extends State<FinishSetupComp> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Placeholder();
|
||||||
|
}
|
||||||
|
}
|
||||||
140
lib/src/visual/views/onboarding/setup/profile_setup.view.dart
Normal file
140
lib/src/visual/views/onboarding/setup/profile_setup.view.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
import 'package:avatar_maker/avatar_maker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:vector_graphics/vector_graphics.dart';
|
||||||
|
import '../setup.view.dart';
|
||||||
|
|
||||||
|
class ProfileSetupPage extends StatefulWidget {
|
||||||
|
const ProfileSetupPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProfileSetupPage> createState() => _ProfileSetupPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
||||||
|
final AvatarMakerController _avatarMakerController =
|
||||||
|
PersistentAvatarMakerController(customizedPropertyCategories: []);
|
||||||
|
late final TextEditingController _displayNameController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_displayNameController = TextEditingController(
|
||||||
|
text: userService.currentUser.displayName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_displayNameController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.lang.onboardingProfileTitle,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
context.lang.onboardingProfileBody,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: context.color.primary.withValues(alpha: 0.2),
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: userService.currentUser.avatarSvg == null
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(80),
|
||||||
|
child: Container(
|
||||||
|
width: 160,
|
||||||
|
height: 160,
|
||||||
|
color: context.color.surfaceContainer,
|
||||||
|
child: const SvgPicture(
|
||||||
|
AssetBytesLoader('assets/images/default_avatar.svg.vec'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: AvatarMakerAvatar(
|
||||||
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
radius: 80,
|
||||||
|
controller: _avatarMakerController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
await context.push(Routes.settingsProfileModifyAvatar);
|
||||||
|
await _avatarMakerController.performRestore();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.palette_outlined),
|
||||||
|
label: Text(context.lang.settingsProfileCustomizeAvatar),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
TextField(
|
||||||
|
controller: _displayNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: context.lang.settingsProfileEditDisplayName,
|
||||||
|
hintText: context.lang.settingsProfileEditDisplayNameNew,
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
|
filled: true,
|
||||||
|
fillColor: context.color.surfaceContainerLow,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await UserService.update((user) {
|
||||||
|
if (_displayNameController.text.isNotEmpty) {
|
||||||
|
user.displayName = _displayNameController.text;
|
||||||
|
}
|
||||||
|
user.currentSetupPage = SetupPages.profile.next()?.name;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
backgroundColor: context.color.primary,
|
||||||
|
foregroundColor: context.color.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
context.lang.next,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import '../setup.view.dart';
|
||||||
|
|
||||||
|
class UserDiscoverySetupPage extends StatelessWidget {
|
||||||
|
const UserDiscoverySetupPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.lang.onboardingUserDiscoveryTitle,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await UserService.update((user) {
|
||||||
|
user.currentSetupPage = SetupPages.profile.name;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(200, 50),
|
||||||
|
backgroundColor: context.color.primary,
|
||||||
|
foregroundColor: context.color.onPrimary,
|
||||||
|
),
|
||||||
|
child: Text(context.lang.onboardingResetSetup),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||||
|
|
||||||
|
class VerificationBadgeSetupPage extends StatelessWidget {
|
||||||
|
const VerificationBadgeSetupPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.lang.onboardingVerificationBadgeTitle,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
const VerificationBadgeInfo(),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await UserService.update((user) {
|
||||||
|
user.currentSetupPage = SetupPages.verificationBadge.next()?.name;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
backgroundColor: context.color.primary,
|
||||||
|
foregroundColor: context.color.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
context.lang.understood,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
|
@ -8,7 +7,7 @@ import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/services/backup/common.backup.dart';
|
import 'package:twonly/src/services/backup/common.backup.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
import 'package:twonly/src/visual/views/settings/backup/components/backup_setup.comp.dart';
|
||||||
|
|
||||||
class SetupBackupView extends StatefulWidget {
|
class SetupBackupView extends StatefulWidget {
|
||||||
const SetupBackupView({
|
const SetupBackupView({
|
||||||
|
|
@ -25,7 +24,6 @@ class SetupBackupView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SetupBackupViewState extends State<SetupBackupView> {
|
class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
bool obscureText = true;
|
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
final TextEditingController passwordCtrl = TextEditingController();
|
final TextEditingController passwordCtrl = TextEditingController();
|
||||||
final TextEditingController repeatedPasswordCtrl = TextEditingController();
|
final TextEditingController repeatedPasswordCtrl = TextEditingController();
|
||||||
|
|
@ -53,10 +51,6 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
|
||||||
isLoading = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
await enableTwonlySafe(passwordCtrl.text);
|
await enableTwonlySafe(passwordCtrl.text);
|
||||||
|
|
||||||
|
|
@ -105,80 +99,28 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
Stack(
|
BackupPasswordTextField(
|
||||||
children: [
|
controller: passwordCtrl,
|
||||||
TextField(
|
labelText: context.lang.password,
|
||||||
controller: passwordCtrl,
|
onChanged: (value) => setState(() {}),
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
style: const TextStyle(fontSize: 17),
|
|
||||||
obscureText: obscureText,
|
|
||||||
decoration: getInputDecoration(
|
|
||||||
context,
|
|
||||||
context.lang.password,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
obscureText = !obscureText;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: FaIcon(
|
|
||||||
obscureText
|
|
||||||
? FontAwesomeIcons.eye
|
|
||||||
: FontAwesomeIcons.eyeSlash,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
Padding(
|
PasswordRequirementText(
|
||||||
padding: const EdgeInsetsGeometry.all(5),
|
text: context.lang.backupPasswordRequirement,
|
||||||
child: Text(
|
showError:
|
||||||
context.lang.backupPasswordRequirement,
|
passwordCtrl.text.length < 8 &&
|
||||||
style: TextStyle(
|
passwordCtrl.text.isNotEmpty,
|
||||||
fontSize: 13,
|
|
||||||
color:
|
|
||||||
(passwordCtrl.text.length < 8 &&
|
|
||||||
passwordCtrl.text.isNotEmpty)
|
|
||||||
? Colors.red
|
|
||||||
: Colors.transparent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
TextField(
|
BackupPasswordTextField(
|
||||||
controller: repeatedPasswordCtrl,
|
controller: repeatedPasswordCtrl,
|
||||||
onChanged: (value) {
|
labelText: context.lang.passwordRepeated,
|
||||||
setState(() {});
|
onChanged: (value) => setState(() {}),
|
||||||
},
|
|
||||||
style: const TextStyle(fontSize: 17),
|
|
||||||
obscureText: true,
|
|
||||||
decoration: getInputDecoration(
|
|
||||||
context,
|
|
||||||
context.lang.passwordRepeated,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
PasswordRequirementText(
|
||||||
padding: const EdgeInsetsGeometry.all(5),
|
text: context.lang.passwordRepeatedNotEqual,
|
||||||
child: Text(
|
showError:
|
||||||
context.lang.passwordRepeatedNotEqual,
|
passwordCtrl.text != repeatedPasswordCtrl.text &&
|
||||||
style: TextStyle(
|
repeatedPasswordCtrl.text.isNotEmpty,
|
||||||
fontSize: 13,
|
|
||||||
color:
|
|
||||||
(passwordCtrl.text != repeatedPasswordCtrl.text &&
|
|
||||||
repeatedPasswordCtrl.text.isNotEmpty)
|
|
||||||
? Colors.red
|
|
||||||
: Colors.transparent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Center(
|
Center(
|
||||||
|
|
@ -239,17 +181,3 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isSecurePassword(String password) async {
|
|
||||||
final badPasswordsStr = await rootBundle.loadString(
|
|
||||||
'assets/passwords/bad_passwords.txt',
|
|
||||||
);
|
|
||||||
final badPasswords = badPasswordsStr.split('\n');
|
|
||||||
if (badPasswords.contains(password)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Check if the password meets all criteria
|
|
||||||
return RegExp('[A-Z]').hasMatch(password) &&
|
|
||||||
RegExp('[a-z]').hasMatch(password) &&
|
|
||||||
RegExp('[0-9]').hasMatch(password);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
|
Future<bool> isSecurePassword(String password) async {
|
||||||
|
final badPasswordsStr = await rootBundle.loadString(
|
||||||
|
'assets/passwords/bad_passwords.txt',
|
||||||
|
);
|
||||||
|
final badPasswords = badPasswordsStr.split('\n');
|
||||||
|
if (badPasswords.contains(password)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check if the password meets all criteria
|
||||||
|
return RegExp('[A-Z]').hasMatch(password) &&
|
||||||
|
RegExp('[a-z]').hasMatch(password) &&
|
||||||
|
RegExp('[0-9]').hasMatch(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackupPasswordTextField extends StatefulWidget {
|
||||||
|
const BackupPasswordTextField({
|
||||||
|
required this.controller,
|
||||||
|
required this.labelText,
|
||||||
|
this.onChanged,
|
||||||
|
this.obscureByDefault = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String labelText;
|
||||||
|
final ValueChanged<String>? onChanged;
|
||||||
|
final bool obscureByDefault;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BackupPasswordTextField> createState() => _BackupPasswordTextFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackupPasswordTextFieldState extends State<BackupPasswordTextField> {
|
||||||
|
late bool _obscureText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_obscureText = widget.obscureByDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextField(
|
||||||
|
controller: widget.controller,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
obscureText: _obscureText,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.labelText,
|
||||||
|
filled: true,
|
||||||
|
fillColor: context.color.surfaceContainerLow,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_obscureText = !_obscureText;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: FaIcon(
|
||||||
|
_obscureText ? FontAwesomeIcons.eye : FontAwesomeIcons.eyeSlash,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordRequirementText extends StatelessWidget {
|
||||||
|
const PasswordRequirementText({
|
||||||
|
required this.text,
|
||||||
|
required this.showError,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final bool showError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: showError ? Colors.red : Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
|
||||||
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
|
||||||
|
|
||||||
const colorVerificationBadgeYellow = Color.fromARGB(255, 0, 182, 238);
|
|
||||||
|
|
||||||
class VerificationBadeFaqView extends StatefulWidget {
|
|
||||||
const VerificationBadeFaqView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<VerificationBadeFaqView> createState() =>
|
|
||||||
_VerificationBadeFaqViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VerificationBadeFaqViewState extends State<VerificationBadeFaqView> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(context.lang.verificationBadgeTitle),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
padding: const EdgeInsets.all(40),
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.lang.verificationBadgeGeneralDesc,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 40),
|
|
||||||
_buildItem(
|
|
||||||
icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40),
|
|
||||||
description: context.lang.verificationBadgeGreenDesc,
|
|
||||||
),
|
|
||||||
_buildItem(
|
|
||||||
icon: const SvgIcon(
|
|
||||||
assetPath: SvgIcons.verifiedGreen,
|
|
||||||
size: 40,
|
|
||||||
color: colorVerificationBadgeYellow,
|
|
||||||
),
|
|
||||||
description: context.lang.verificationBadgeYellowDesc,
|
|
||||||
),
|
|
||||||
_buildItem(
|
|
||||||
icon: const SvgIcon(assetPath: SvgIcons.verifiedRed, size: 40),
|
|
||||||
description: context.lang.verificationBadgeRedDesc,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
BetterListTile(
|
|
||||||
leading: const FaIcon(FontAwesomeIcons.camera),
|
|
||||||
text: context.lang.scanOtherProfile,
|
|
||||||
onTap: () => context.push(Routes.cameraQRScanner),
|
|
||||||
),
|
|
||||||
BetterListTile(
|
|
||||||
leading: const FaIcon(FontAwesomeIcons.qrcode),
|
|
||||||
text: context.lang.openYourOwnQRcode,
|
|
||||||
onTap: () => context.push(Routes.settingsPublicProfile),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildItem({required Widget icon, required String description}) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 25),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
icon,
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
description,
|
|
||||||
style: const TextStyle(fontSize: 16, height: 1.4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
||||||
|
|
||||||
|
class VerificationBadeFaqView extends StatefulWidget {
|
||||||
|
const VerificationBadeFaqView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VerificationBadeFaqView> createState() =>
|
||||||
|
_VerificationBadeFaqViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VerificationBadeFaqViewState extends State<VerificationBadeFaqView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(context.lang.verificationBadgeTitle),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(40),
|
||||||
|
children: [
|
||||||
|
const VerificationBadgeInfo(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
BetterListTile(
|
||||||
|
leading: const FaIcon(FontAwesomeIcons.camera),
|
||||||
|
text: context.lang.scanOtherProfile,
|
||||||
|
onTap: () => context.push(Routes.cameraQRScanner),
|
||||||
|
),
|
||||||
|
BetterListTile(
|
||||||
|
leading: const FaIcon(FontAwesomeIcons.qrcode),
|
||||||
|
text: context.lang.openYourOwnQRcode,
|
||||||
|
onTap: () => context.push(Routes.settingsPublicProfile),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,7 @@ void main() {
|
||||||
username: 'test_user',
|
username: 'test_user',
|
||||||
displayName: 'Test User',
|
displayName: 'Test User',
|
||||||
subscriptionPlan: 'Free',
|
subscriptionPlan: 'Free',
|
||||||
|
currentSetupPage: null,
|
||||||
)..appVersion = 62;
|
)..appVersion = 62;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ void main() {
|
||||||
username: 'test_user',
|
username: 'test_user',
|
||||||
displayName: 'Test User',
|
displayName: 'Test User',
|
||||||
subscriptionPlan: 'Free',
|
subscriptionPlan: 'Free',
|
||||||
|
currentSetupPage: null,
|
||||||
)..appVersion = 100;
|
)..appVersion = 100;
|
||||||
userService.isUserCreated = true;
|
userService.isUserCreated = true;
|
||||||
AppEnvironment.initTesting();
|
AppEnvironment.initTesting();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue