From c47c91c1bae09371130ab36c8c06543168e0ba9b Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 28 Apr 2026 00:15:05 +0200 Subject: [PATCH] starting with a proper setup --- CHANGELOG.md | 1 + lib/app.dart | 16 +- lib/globals.dart | 2 + lib/main.dart | 10 +- .../generated/app_localizations.dart | 60 +++++- .../generated/app_localizations_de.dart | 35 +++- .../generated/app_localizations_en.dart | 35 +++- .../generated/app_localizations_sv.dart | 35 +++- lib/src/localization/translations | 2 +- lib/src/model/json/userdata.model.dart | 6 + lib/src/model/json/userdata.model.g.dart | 6 +- lib/src/providers/routing.provider.dart | 2 +- lib/src/utils/misc.dart | 13 +- .../components/verification_badge.comp.dart | 2 +- .../verification_badge_info.comp.dart | 74 ++++++++ .../views/onboarding/register.view.dart | 5 +- .../visual/views/onboarding/setup.view.dart | 130 +++++++++++++ .../onboarding/setup/backup_setup.view.dart | 172 ++++++++++++++++++ .../onboarding/setup/finish_setup.comp.dart | 15 ++ .../onboarding/setup/profile_setup.view.dart | 140 ++++++++++++++ .../setup/user_discovery_setup.view.dart | 37 ++++ .../setup/verification_badge_setup.view.dart | 50 +++++ .../settings/backup/backup_setup.view.dart | 108 ++--------- .../backup/components/backup_setup.comp.dart | 101 ++++++++++ .../help/faq/verification_bade_faq.view.dart | 83 --------- .../help/faq/verification_badge_faq.view.dart | 43 +++++ test/features/flame_counter_test.dart | 1 + test/services/group_services_test.dart | 1 + 28 files changed, 979 insertions(+), 206 deletions(-) create mode 100644 lib/src/visual/components/verification_badge_info.comp.dart create mode 100644 lib/src/visual/views/onboarding/setup.view.dart create mode 100644 lib/src/visual/views/onboarding/setup/backup_setup.view.dart create mode 100644 lib/src/visual/views/onboarding/setup/finish_setup.comp.dart create mode 100644 lib/src/visual/views/onboarding/setup/profile_setup.view.dart create mode 100644 lib/src/visual/views/onboarding/setup/user_discovery_setup.view.dart create mode 100644 lib/src/visual/views/onboarding/setup/verification_badge_setup.view.dart create mode 100644 lib/src/visual/views/settings/backup/components/backup_setup.comp.dart delete mode 100644 lib/src/visual/views/settings/help/faq/verification_bade_faq.view.dart create mode 100644 lib/src/visual/views/settings/help/faq/verification_badge_faq.view.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index a40532ec..96f975ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/app.dart b/lib/app.dart index 73d95f9a..080994d3 100644 --- a/lib/app.dart +++ b/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 { bool _showOnboarding = true; bool _isLoaded = false; - bool _skipBackup = kDebugMode; bool _isTwonlyLocked = true; (Future?, bool) _proofOfWork = (null, false); @@ -171,11 +169,13 @@ class _AppMainWidgetState extends State { _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 { diff --git a/lib/globals.dart b/lib/globals.dart index 94eaadce..c77aec37 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -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 { diff --git a/lib/main.dart b/lib/main.dart index 1fcbf35b..c935cfc9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 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; } diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index d84ed265..0b260764 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -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. diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 4199501e..c5dfa2e6 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -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) { diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 33f4a9e9..4ad9d711 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -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) { diff --git a/lib/src/localization/generated/app_localizations_sv.dart b/lib/src/localization/generated/app_localizations_sv.dart index fc9cce99..df6b315b 100644 --- a/lib/src/localization/generated/app_localizations_sv.dart +++ b/lib/src/localization/generated/app_localizations_sv.dart @@ -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) { diff --git a/lib/src/localization/translations b/lib/src/localization/translations index e50fdde3..82c248ae 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit e50fdde3db3a81f2db03a03e7f64d6351cc507a4 +Subproject commit 82c248ae18ffda0bf161fbb69ddb3fb30cfc1531 diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 3c249e9c..dc3e7cea 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -9,6 +9,7 @@ class UserData { required this.username, required this.displayName, required this.subscriptionPlan, + required this.currentSetupPage, }); factory UserData.fromJson(Map 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 toJson() => _$UserDataToJson(this); } diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index 98e8bc7e..b81f23e3 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -12,6 +12,7 @@ UserData _$UserDataFromJson(Map 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 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 _$UserDataToJson(UserData instance) => { 'userId': instance.userId, @@ -145,6 +147,8 @@ Map _$UserDataToJson(UserData instance) => { 'userStudyParticipantsToken': instance.userStudyParticipantsToken, 'lastUserStudyDataUpload': instance.lastUserStudyDataUpload ?.toIso8601String(), + 'currentSetupPage': instance.currentSetupPage, + 'skipSetupPages': instance.skipSetupPages, }; const _$ThemeModeEnumMap = { diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index d7c83b3a..cccb4c4c 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -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'; diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index e4157995..33874fc9 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -290,9 +290,11 @@ Future> sha256File(File file) async { return sha256Sink.events.single.bytes; } -List formattedText(BuildContext context, String input) { - // Access the current theme's text color - // Defaulting to bodyMedium color, but you can use labelLarge, displaySmall, etc. +List formattedText( + BuildContext context, + String input, { + Color? boldTextColor, +}) { final defaultColor = Theme.of(context).colorScheme.onSurface; final regex = RegExp(r'\*(.*?)\*'); @@ -301,7 +303,6 @@ List 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 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 formattedText(BuildContext context, String input) { lastMatchEnd = match.end; } - // Add any remaining text after the last match if (lastMatchEnd < input.length) { spans.add( TextSpan( diff --git a/lib/src/visual/components/verification_badge.comp.dart b/lib/src/visual/components/verification_badge.comp.dart index 0d50513c..ad8c26a0 100644 --- a/lib/src/visual/components/verification_badge.comp.dart +++ b/lib/src/visual/components/verification_badge.comp.dart @@ -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({ diff --git a/lib/src/visual/components/verification_badge_info.comp.dart b/lib/src/visual/components/verification_badge_info.comp.dart new file mode 100644 index 00000000..cded90c2 --- /dev/null +++ b/lib/src/visual/components/verification_badge_info.comp.dart @@ -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), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/visual/views/onboarding/register.view.dart b/lib/src/visual/views/onboarding/register.view.dart index e687fca9..01deebc8 100644 --- a/lib/src/visual/views/onboarding/register.view.dart +++ b/lib/src/visual/views/onboarding/register.view.dart @@ -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 { username: username, displayName: username, subscriptionPlan: 'Preview', - )..appVersion = 62; + currentSetupPage: SetupPages.profile.name, + )..appVersion = AppState.latestAppVersionId; await SecureStorage.instance.write( key: SecureStorageKeys.userData, diff --git a/lib/src/visual/views/onboarding/setup.view.dart b/lib/src/visual/views/onboarding/setup.view.dart new file mode 100644 index 00000000..3ac2b44d --- /dev/null +++ b/lib/src/visual/views/onboarding/setup.view.dart @@ -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 createState() => _SetupViewState(); +} + +class _SetupViewState extends State { + @override + Widget build(BuildContext context) { + return StreamBuilder( + 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(); + } + } +} diff --git a/lib/src/visual/views/onboarding/setup/backup_setup.view.dart b/lib/src/visual/views/onboarding/setup/backup_setup.view.dart new file mode 100644 index 00000000..c2dbc6f6 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/backup_setup.view.dart @@ -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 createState() => _BackupSetupPageState(); +} + +class _BackupSetupPageState extends State { + bool isLoading = false; + final TextEditingController passwordCtrl = TextEditingController(); + final TextEditingController repeatedPasswordCtrl = TextEditingController(); + + Future 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(Colors.white), + ), + ) + : Text( + context.lang.next, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup/finish_setup.comp.dart b/lib/src/visual/views/onboarding/setup/finish_setup.comp.dart new file mode 100644 index 00000000..deff6591 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/finish_setup.comp.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class FinishSetupComp extends StatefulWidget { + const FinishSetupComp({super.key}); + + @override + State createState() => _FinishSetupCompState(); +} + +class _FinishSetupCompState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/src/visual/views/onboarding/setup/profile_setup.view.dart b/lib/src/visual/views/onboarding/setup/profile_setup.view.dart new file mode 100644 index 00000000..344a34a0 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/profile_setup.view.dart @@ -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 createState() => _ProfileSetupPageState(); +} + +class _ProfileSetupPageState extends State { + 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, + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup/user_discovery_setup.view.dart b/lib/src/visual/views/onboarding/setup/user_discovery_setup.view.dart new file mode 100644 index 00000000..54533cf5 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/user_discovery_setup.view.dart @@ -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), + ), + ], + ), + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup/verification_badge_setup.view.dart b/lib/src/visual/views/onboarding/setup/verification_badge_setup.view.dart new file mode 100644 index 00000000..4a9eaac5 --- /dev/null +++ b/lib/src/visual/views/onboarding/setup/verification_badge_setup.view.dart @@ -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, + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/visual/views/settings/backup/backup_setup.view.dart b/lib/src/visual/views/settings/backup/backup_setup.view.dart index 25d94181..52968435 100644 --- a/lib/src/visual/views/settings/backup/backup_setup.view.dart +++ b/lib/src/visual/views/settings/backup/backup_setup.view.dart @@ -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 { - bool obscureText = true; bool isLoading = false; final TextEditingController passwordCtrl = TextEditingController(); final TextEditingController repeatedPasswordCtrl = TextEditingController(); @@ -53,10 +51,6 @@ class _SetupBackupViewState extends State { } } - setState(() { - isLoading = true; - }); - await Future.delayed(const Duration(milliseconds: 100)); await enableTwonlySafe(passwordCtrl.text); @@ -105,80 +99,28 @@ class _SetupBackupViewState extends State { 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 { ); } } - -Future 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); -} diff --git a/lib/src/visual/views/settings/backup/components/backup_setup.comp.dart b/lib/src/visual/views/settings/backup/components/backup_setup.comp.dart new file mode 100644 index 00000000..d62ce050 --- /dev/null +++ b/lib/src/visual/views/settings/backup/components/backup_setup.comp.dart @@ -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 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? onChanged; + final bool obscureByDefault; + + @override + State createState() => _BackupPasswordTextFieldState(); +} + +class _BackupPasswordTextFieldState extends State { + 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, + ), + ), + ); + } +} diff --git a/lib/src/visual/views/settings/help/faq/verification_bade_faq.view.dart b/lib/src/visual/views/settings/help/faq/verification_bade_faq.view.dart deleted file mode 100644 index 06428216..00000000 --- a/lib/src/visual/views/settings/help/faq/verification_bade_faq.view.dart +++ /dev/null @@ -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 createState() => - _VerificationBadeFaqViewState(); -} - -class _VerificationBadeFaqViewState extends State { - @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), - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/visual/views/settings/help/faq/verification_badge_faq.view.dart b/lib/src/visual/views/settings/help/faq/verification_badge_faq.view.dart new file mode 100644 index 00000000..809e1310 --- /dev/null +++ b/lib/src/visual/views/settings/help/faq/verification_badge_faq.view.dart @@ -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 createState() => + _VerificationBadeFaqViewState(); +} + +class _VerificationBadeFaqViewState extends State { + @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), + ), + ], + ), + ); + } +} diff --git a/test/features/flame_counter_test.dart b/test/features/flame_counter_test.dart index 52cf7058..25554e73 100644 --- a/test/features/flame_counter_test.dart +++ b/test/features/flame_counter_test.dart @@ -57,6 +57,7 @@ void main() { username: 'test_user', displayName: 'Test User', subscriptionPlan: 'Free', + currentSetupPage: null, )..appVersion = 62; }); diff --git a/test/services/group_services_test.dart b/test/services/group_services_test.dart index 4e426762..ffe8f65f 100644 --- a/test/services/group_services_test.dart +++ b/test/services/group_services_test.dart @@ -48,6 +48,7 @@ void main() { username: 'test_user', displayName: 'Test User', subscriptionPlan: 'Free', + currentSetupPage: null, )..appVersion = 100; userService.isUserCreated = true; AppEnvironment.initTesting();