mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 01:32:13 +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: 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
|
||||
- Fix: Many smaller issues
|
||||
|
||||
|
|
|
|||
16
lib/app.dart
16
lib/app.dart
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.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/onboarding/onboarding.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';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
|
|
@ -119,7 +118,6 @@ class AppMainWidget extends StatefulWidget {
|
|||
class _AppMainWidgetState extends State<AppMainWidget> {
|
||||
bool _showOnboarding = true;
|
||||
bool _isLoaded = false;
|
||||
bool _skipBackup = kDebugMode;
|
||||
bool _isTwonlyLocked = true;
|
||||
|
||||
(Future<int>?, bool) _proofOfWork = (null, false);
|
||||
|
|
@ -171,11 +169,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
_isTwonlyLocked = false;
|
||||
}),
|
||||
);
|
||||
} else if (userService.currentUser.twonlySafeBackup == null &&
|
||||
!_skipBackup) {
|
||||
child = SetupBackupView(
|
||||
callBack: () => setState(() {
|
||||
_skipBackup = true;
|
||||
} else if (true ||
|
||||
!userService.currentUser.skipSetupPages &&
|
||||
userService.currentUser.currentSetupPage ==
|
||||
SetupPages.profile.name) {
|
||||
child = SetupView(
|
||||
onUpdate: () => setState(() {
|
||||
// userService.currentUser has updated...
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ class AppState {
|
|||
static bool isInBackgroundTask = false;
|
||||
static bool allowErrorTrackingViaSentry = false;
|
||||
static bool gotMessageFromServer = false;
|
||||
// initialized in runMigrations (main.dart)
|
||||
static late int latestAppVersionId;
|
||||
}
|
||||
|
||||
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/log.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
|
||||
/// 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'**
|
||||
String get enable;
|
||||
|
||||
/// No description provided for @understood.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Understood'**
|
||||
String get understood;
|
||||
|
||||
/// No description provided for @cancel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -2878,6 +2884,54 @@ abstract class AppLocalizations {
|
|||
/// **'Skip for now'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3079,19 +3133,19 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @verificationBadgeGreenDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A contact you have personally verified.'**
|
||||
/// **'A contact you have *personally* verified.'**
|
||||
String get verificationBadgeGreenDesc;
|
||||
|
||||
/// No description provided for @verificationBadgeYellowDesc.
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// No description provided for @verificationBadgeRedDesc.
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// No description provided for @chatEntryFlameRestored.
|
||||
|
|
|
|||
|
|
@ -575,6 +575,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get enable => 'Aktivieren';
|
||||
|
||||
@override
|
||||
String get understood => 'Verstanden';
|
||||
|
||||
@override
|
||||
String get cancel => 'Abbrechen';
|
||||
|
||||
|
|
@ -1588,6 +1591,32 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String linkFromUsername(Object username) {
|
||||
return 'Ist der Link von $username?';
|
||||
|
|
@ -1721,15 +1750,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get verificationBadgeGreenDesc =>
|
||||
'Ein Kontakt, den du persönlich verifiziert hast.';
|
||||
'Ein Kontakt, den du *persönlich verifiziert* hast.';
|
||||
|
||||
@override
|
||||
String get verificationBadgeYellowDesc =>
|
||||
'Ein Kontakt, der von mind. einem deiner Kontakte verifiziert wurde.';
|
||||
'Ein Kontakt, der von mind. einem *deiner Kontakte verifiziert* wurde.';
|
||||
|
||||
@override
|
||||
String get verificationBadgeRedDesc =>
|
||||
'Ein Kontakt, dessen Identität noch nicht überprüft wurde.';
|
||||
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
|
||||
|
||||
@override
|
||||
String chatEntryFlameRestored(Object count) {
|
||||
|
|
|
|||
|
|
@ -570,6 +570,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get enable => 'Enable';
|
||||
|
||||
@override
|
||||
String get understood => 'Understood';
|
||||
|
||||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
|
|
@ -1578,6 +1581,32 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String linkFromUsername(Object username) {
|
||||
return 'Is the link from $username?';
|
||||
|
|
@ -1709,15 +1738,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get verificationBadgeGreenDesc =>
|
||||
'A contact you have personally verified.';
|
||||
'A contact you have *personally* verified.';
|
||||
|
||||
@override
|
||||
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
|
||||
String get verificationBadgeRedDesc =>
|
||||
'A contact whose identity has not yet been verified.';
|
||||
'A contact whose identity has *not* yet been verified.';
|
||||
|
||||
@override
|
||||
String chatEntryFlameRestored(Object count) {
|
||||
|
|
|
|||
|
|
@ -570,6 +570,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get enable => 'Enable';
|
||||
|
||||
@override
|
||||
String get understood => 'Understood';
|
||||
|
||||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
|
|
@ -1578,6 +1581,32 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String linkFromUsername(Object username) {
|
||||
return 'Is the link from $username?';
|
||||
|
|
@ -1709,15 +1738,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get verificationBadgeGreenDesc =>
|
||||
'A contact you have personally verified.';
|
||||
'A contact you have *personally* verified.';
|
||||
|
||||
@override
|
||||
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
|
||||
String get verificationBadgeRedDesc =>
|
||||
'A contact whose identity has not yet been verified.';
|
||||
'A contact whose identity has *not* yet been verified.';
|
||||
|
||||
@override
|
||||
String chatEntryFlameRestored(Object count) {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e50fdde3db3a81f2db03a03e7f64d6351cc507a4
|
||||
Subproject commit 82c248ae18ffda0bf161fbb69ddb3fb30cfc1531
|
||||
|
|
@ -9,6 +9,7 @@ class UserData {
|
|||
required this.username,
|
||||
required this.displayName,
|
||||
required this.subscriptionPlan,
|
||||
required this.currentSetupPage,
|
||||
});
|
||||
factory UserData.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserDataFromJson(json);
|
||||
|
|
@ -136,6 +137,11 @@ class UserData {
|
|||
// Once a day the anonymous data is collected and send to the server
|
||||
DateTime? lastUserStudyDataUpload;
|
||||
|
||||
String? currentSetupPage;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool skipSetupPages = false;
|
||||
|
||||
Map<String, dynamic> toJson() => _$UserDataToJson(this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
username: json['username'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
|
||||
currentSetupPage: json['currentSetupPage'] as String?,
|
||||
)
|
||||
..avatarSvg = json['avatarSvg'] as String?
|
||||
..avatarJson = json['avatarJson'] as String?
|
||||
|
|
@ -93,7 +94,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
json['userStudyParticipantsToken'] as String?
|
||||
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == 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>{
|
||||
'userId': instance.userId,
|
||||
|
|
@ -145,6 +147,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'userStudyParticipantsToken': instance.userStudyParticipantsToken,
|
||||
'lastUserStudyDataUpload': instance.lastUserStudyDataUpload
|
||||
?.toIso8601String(),
|
||||
'currentSetupPage': instance.currentSetupPage,
|
||||
'skipSetupPages': instance.skipSetupPages,
|
||||
};
|
||||
|
||||
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/diagnostics.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/notification.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;
|
||||
}
|
||||
|
||||
List<TextSpan> formattedText(BuildContext context, String input) {
|
||||
// Access the current theme's text color
|
||||
// Defaulting to bodyMedium color, but you can use labelLarge, displaySmall, etc.
|
||||
List<TextSpan> formattedText(
|
||||
BuildContext context,
|
||||
String input, {
|
||||
Color? boldTextColor,
|
||||
}) {
|
||||
final defaultColor = Theme.of(context).colorScheme.onSurface;
|
||||
|
||||
final regex = RegExp(r'\*(.*?)\*');
|
||||
|
|
@ -301,7 +303,6 @@ List<TextSpan> formattedText(BuildContext context, String input) {
|
|||
var lastMatchEnd = 0;
|
||||
|
||||
for (final match in regex.allMatches(input)) {
|
||||
// Add text before the match (Normal style)
|
||||
if (match.start > lastMatchEnd) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
|
|
@ -311,13 +312,12 @@ List<TextSpan> formattedText(BuildContext context, String input) {
|
|||
);
|
||||
}
|
||||
|
||||
// Add the matched text (Bold style)
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: match.group(1),
|
||||
style: TextStyle(
|
||||
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;
|
||||
}
|
||||
|
||||
// Add any remaining text after the last match
|
||||
if (lastMatchEnd < input.length) {
|
||||
spans.add(
|
||||
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/twonly.db.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/views/settings/help/faq/verification_bade_faq.view.dart';
|
||||
|
||||
class VerificationBadgeComp extends StatefulWidget {
|
||||
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/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.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/visual/components/alert.dialog.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 {
|
||||
const RegisterView({
|
||||
|
|
@ -136,7 +138,8 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
username: username,
|
||||
displayName: username,
|
||||
subscriptionPlan: 'Preview',
|
||||
)..appVersion = 62;
|
||||
currentSetupPage: SetupPages.profile.name,
|
||||
)..appVersion = AppState.latestAppVersionId;
|
||||
|
||||
await SecureStorage.instance.write(
|
||||
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/material.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.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/utils/misc.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 {
|
||||
const SetupBackupView({
|
||||
|
|
@ -25,7 +24,6 @@ class SetupBackupView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SetupBackupViewState extends State<SetupBackupView> {
|
||||
bool obscureText = true;
|
||||
bool isLoading = false;
|
||||
final TextEditingController passwordCtrl = 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 enableTwonlySafe(passwordCtrl.text);
|
||||
|
||||
|
|
@ -105,80 +99,28 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
|||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Stack(
|
||||
children: [
|
||||
TextField(
|
||||
controller: passwordCtrl,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
BackupPasswordTextField(
|
||||
controller: passwordCtrl,
|
||||
labelText: context.lang.password,
|
||||
onChanged: (value) => setState(() {}),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.all(5),
|
||||
child: Text(
|
||||
context.lang.backupPasswordRequirement,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color:
|
||||
(passwordCtrl.text.length < 8 &&
|
||||
passwordCtrl.text.isNotEmpty)
|
||||
? Colors.red
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
PasswordRequirementText(
|
||||
text: context.lang.backupPasswordRequirement,
|
||||
showError:
|
||||
passwordCtrl.text.length < 8 &&
|
||||
passwordCtrl.text.isNotEmpty,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
TextField(
|
||||
BackupPasswordTextField(
|
||||
controller: repeatedPasswordCtrl,
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
style: const TextStyle(fontSize: 17),
|
||||
obscureText: true,
|
||||
decoration: getInputDecoration(
|
||||
context,
|
||||
context.lang.passwordRepeated,
|
||||
),
|
||||
labelText: context.lang.passwordRepeated,
|
||||
onChanged: (value) => setState(() {}),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.all(5),
|
||||
child: Text(
|
||||
context.lang.passwordRepeatedNotEqual,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color:
|
||||
(passwordCtrl.text != repeatedPasswordCtrl.text &&
|
||||
repeatedPasswordCtrl.text.isNotEmpty)
|
||||
? Colors.red
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
PasswordRequirementText(
|
||||
text: context.lang.passwordRepeatedNotEqual,
|
||||
showError:
|
||||
passwordCtrl.text != repeatedPasswordCtrl.text &&
|
||||
repeatedPasswordCtrl.text.isNotEmpty,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
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',
|
||||
displayName: 'Test User',
|
||||
subscriptionPlan: 'Free',
|
||||
currentSetupPage: null,
|
||||
)..appVersion = 62;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ void main() {
|
|||
username: 'test_user',
|
||||
displayName: 'Test User',
|
||||
subscriptionPlan: 'Free',
|
||||
currentSetupPage: null,
|
||||
)..appVersion = 100;
|
||||
userService.isUserCreated = true;
|
||||
AppEnvironment.initTesting();
|
||||
|
|
|
|||
Loading…
Reference in a new issue