starting with a proper setup

This commit is contained in:
otsmr 2026-04-28 00:15:05 +02:00
parent 8021768883
commit c47c91c1ba
28 changed files with 979 additions and 206 deletions

View file

@ -4,6 +4,7 @@
- New: Feature to find friends without a phone number - New: Feature to find friends without a phone number
- New: The verification state is now transferred to the scanned user. - New: The verification state is now transferred to the scanned user.
- New: Registration setup to configure the most important configurations
- Improved: FAQ is now in the app rather than opening in the browser - Improved: FAQ is now in the app rather than opening in the browser
- Fix: Many smaller issues - Fix: Many smaller issues

View file

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -18,7 +17,7 @@ import 'package:twonly/src/visual/views/critical_error.view.dart';
import 'package:twonly/src/visual/views/home.view.dart'; import 'package:twonly/src/visual/views/home.view.dart';
import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/visual/views/onboarding/register.view.dart'; import 'package:twonly/src/visual/views/onboarding/register.view.dart';
import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
import 'package:twonly/src/visual/views/unlock_twonly.view.dart'; import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
@ -119,7 +118,6 @@ class AppMainWidget extends StatefulWidget {
class _AppMainWidgetState extends State<AppMainWidget> { class _AppMainWidgetState extends State<AppMainWidget> {
bool _showOnboarding = true; bool _showOnboarding = true;
bool _isLoaded = false; bool _isLoaded = false;
bool _skipBackup = kDebugMode;
bool _isTwonlyLocked = true; bool _isTwonlyLocked = true;
(Future<int>?, bool) _proofOfWork = (null, false); (Future<int>?, bool) _proofOfWork = (null, false);
@ -171,11 +169,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
_isTwonlyLocked = false; _isTwonlyLocked = false;
}), }),
); );
} else if (userService.currentUser.twonlySafeBackup == null && } else if (true ||
!_skipBackup) { !userService.currentUser.skipSetupPages &&
child = SetupBackupView( userService.currentUser.currentSetupPage ==
callBack: () => setState(() { SetupPages.profile.name) {
_skipBackup = true; child = SetupView(
onUpdate: () => setState(() {
// userService.currentUser has updated...
}), }),
); );
} else { } else {

View file

@ -27,6 +27,8 @@ class AppState {
static bool isInBackgroundTask = false; static bool isInBackgroundTask = false;
static bool allowErrorTrackingViaSentry = false; static bool allowErrorTrackingViaSentry = false;
static bool gotMessageFromServer = false; static bool gotMessageFromServer = false;
// initialized in runMigrations (main.dart)
static late int latestAppVersionId;
} }
class AppGlobalKeys { class AppGlobalKeys {

View file

@ -28,6 +28,7 @@ import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart'; import 'package:twonly/src/utils/secure_storage.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
/// This function is used to initialized the absolute minimum so it /// This function is used to initialized the absolute minimum so it
/// can also be used by the backend without the UI was loaded. /// can also be used by the backend without the UI was loaded.
@ -147,6 +148,13 @@ Future<void> runMigrations() async {
); );
} }
} }
await UserService.update((u) => u.appVersion = 109); await UserService.update((u) {
u
..appVersion = 109
..skipSetupPages = true
..currentSetupPage = SetupPages.userDiscovery.name;
});
} }
AppState.latestAppVersionId = 110;
} }

View file

@ -1126,6 +1126,12 @@ abstract class AppLocalizations {
/// **'Enable'** /// **'Enable'**
String get enable; String get enable;
/// No description provided for @understood.
///
/// In en, this message translates to:
/// **'Understood'**
String get understood;
/// No description provided for @cancel. /// No description provided for @cancel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2878,6 +2884,54 @@ abstract class AppLocalizations {
/// **'Skip for now'** /// **'Skip for now'**
String get skipForNow; String get skipForNow;
/// No description provided for @onboardingFinishLater.
///
/// In en, this message translates to:
/// **'Finish later'**
String get onboardingFinishLater;
/// No description provided for @onboardingProfileTitle.
///
/// In en, this message translates to:
/// **'Choose your look'**
String get onboardingProfileTitle;
/// No description provided for @onboardingProfileBody.
///
/// In en, this message translates to:
/// **'Select an avatar and a display name that friends will see.'**
String get onboardingProfileBody;
/// No description provided for @onboardingBackupTitle.
///
/// In en, this message translates to:
/// **'Backup Setup'**
String get onboardingBackupTitle;
/// No description provided for @onboardingBackupBody.
///
/// In en, this message translates to:
/// **'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.'**
String get onboardingBackupBody;
/// No description provided for @onboardingVerificationBadgeTitle.
///
/// In en, this message translates to:
/// **'Verification Badge'**
String get onboardingVerificationBadgeTitle;
/// No description provided for @onboardingUserDiscoveryTitle.
///
/// In en, this message translates to:
/// **'User Discovery'**
String get onboardingUserDiscoveryTitle;
/// No description provided for @onboardingResetSetup.
///
/// In en, this message translates to:
/// **'Reset Setup'**
String get onboardingResetSetup;
/// No description provided for @linkFromUsername. /// No description provided for @linkFromUsername.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3079,19 +3133,19 @@ abstract class AppLocalizations {
/// No description provided for @verificationBadgeGreenDesc. /// No description provided for @verificationBadgeGreenDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'A contact you have personally verified.'** /// **'A contact you have *personally* verified.'**
String get verificationBadgeGreenDesc; String get verificationBadgeGreenDesc;
/// No description provided for @verificationBadgeYellowDesc. /// No description provided for @verificationBadgeYellowDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'A contact who has been verified by at least one of your contacts.'** /// **'A contact who has been verified by at least one of *your contacts*.'**
String get verificationBadgeYellowDesc; String get verificationBadgeYellowDesc;
/// No description provided for @verificationBadgeRedDesc. /// No description provided for @verificationBadgeRedDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'A contact whose identity has not yet been verified.'** /// **'A contact whose identity has *not* yet been verified.'**
String get verificationBadgeRedDesc; String get verificationBadgeRedDesc;
/// No description provided for @chatEntryFlameRestored. /// No description provided for @chatEntryFlameRestored.

View file

@ -575,6 +575,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get enable => 'Aktivieren'; String get enable => 'Aktivieren';
@override
String get understood => 'Verstanden';
@override @override
String get cancel => 'Abbrechen'; String get cancel => 'Abbrechen';
@ -1588,6 +1591,32 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get skipForNow => 'Vorerst überspringen'; String get skipForNow => 'Vorerst überspringen';
@override
String get onboardingFinishLater => 'Später abschließen';
@override
String get onboardingProfileTitle => 'Wähle deinen Look';
@override
String get onboardingProfileBody =>
'Wähle einen Avatar und einen Anzeigenamen, den deine Freunde sehen werden.';
@override
String get onboardingBackupTitle => 'Backup einrichten';
@override
String get onboardingBackupBody =>
'Sichere deine twonly-Identität, da dies die einzige Möglichkeit ist, dein Konto wiederherzustellen, wenn du die App deinstallierst oder dein Handy verlierst.';
@override
String get onboardingVerificationBadgeTitle => 'Verifizierungs-Abzeichen';
@override
String get onboardingUserDiscoveryTitle => 'Freunde finden';
@override
String get onboardingResetSetup => 'Setup zurücksetzen';
@override @override
String linkFromUsername(Object username) { String linkFromUsername(Object username) {
return 'Ist der Link von $username?'; return 'Ist der Link von $username?';
@ -1721,15 +1750,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get verificationBadgeGreenDesc => String get verificationBadgeGreenDesc =>
'Ein Kontakt, den du persönlich verifiziert hast.'; 'Ein Kontakt, den du *persönlich verifiziert* hast.';
@override @override
String get verificationBadgeYellowDesc => String get verificationBadgeYellowDesc =>
'Ein Kontakt, der von mind. einem deiner Kontakte verifiziert wurde.'; 'Ein Kontakt, der von mind. einem *deiner Kontakte verifiziert* wurde.';
@override @override
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'Ein Kontakt, dessen Identität noch nicht überprüft wurde.'; 'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
@override @override
String chatEntryFlameRestored(Object count) { String chatEntryFlameRestored(Object count) {

View file

@ -570,6 +570,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get enable => 'Enable'; String get enable => 'Enable';
@override
String get understood => 'Understood';
@override @override
String get cancel => 'Cancel'; String get cancel => 'Cancel';
@ -1578,6 +1581,32 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get skipForNow => 'Skip for now'; String get skipForNow => 'Skip for now';
@override
String get onboardingFinishLater => 'Finish later';
@override
String get onboardingProfileTitle => 'Choose your look';
@override
String get onboardingProfileBody =>
'Select an avatar and a display name that friends will see.';
@override
String get onboardingBackupTitle => 'Backup Setup';
@override
String get onboardingBackupBody =>
'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.';
@override
String get onboardingVerificationBadgeTitle => 'Verification Badge';
@override
String get onboardingUserDiscoveryTitle => 'User Discovery';
@override
String get onboardingResetSetup => 'Reset Setup';
@override @override
String linkFromUsername(Object username) { String linkFromUsername(Object username) {
return 'Is the link from $username?'; return 'Is the link from $username?';
@ -1709,15 +1738,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get verificationBadgeGreenDesc => String get verificationBadgeGreenDesc =>
'A contact you have personally verified.'; 'A contact you have *personally* verified.';
@override @override
String get verificationBadgeYellowDesc => String get verificationBadgeYellowDesc =>
'A contact who has been verified by at least one of your contacts.'; 'A contact who has been verified by at least one of *your contacts*.';
@override @override
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'A contact whose identity has not yet been verified.'; 'A contact whose identity has *not* yet been verified.';
@override @override
String chatEntryFlameRestored(Object count) { String chatEntryFlameRestored(Object count) {

View file

@ -570,6 +570,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get enable => 'Enable'; String get enable => 'Enable';
@override
String get understood => 'Understood';
@override @override
String get cancel => 'Cancel'; String get cancel => 'Cancel';
@ -1578,6 +1581,32 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get skipForNow => 'Skip for now'; String get skipForNow => 'Skip for now';
@override
String get onboardingFinishLater => 'Finish later';
@override
String get onboardingProfileTitle => 'Choose your look';
@override
String get onboardingProfileBody =>
'Select an avatar and a display name that friends will see.';
@override
String get onboardingBackupTitle => 'Backup Setup';
@override
String get onboardingBackupBody =>
'Back up your twonly identity, as this is the only way to restore your account if you uninstall the app or lose your phone.';
@override
String get onboardingVerificationBadgeTitle => 'Verification Badge';
@override
String get onboardingUserDiscoveryTitle => 'User Discovery';
@override
String get onboardingResetSetup => 'Reset Setup';
@override @override
String linkFromUsername(Object username) { String linkFromUsername(Object username) {
return 'Is the link from $username?'; return 'Is the link from $username?';
@ -1709,15 +1738,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get verificationBadgeGreenDesc => String get verificationBadgeGreenDesc =>
'A contact you have personally verified.'; 'A contact you have *personally* verified.';
@override @override
String get verificationBadgeYellowDesc => String get verificationBadgeYellowDesc =>
'A contact who has been verified by at least one of your contacts.'; 'A contact who has been verified by at least one of *your contacts*.';
@override @override
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'A contact whose identity has not yet been verified.'; 'A contact whose identity has *not* yet been verified.';
@override @override
String chatEntryFlameRestored(Object count) { String chatEntryFlameRestored(Object count) {

@ -1 +1 @@
Subproject commit e50fdde3db3a81f2db03a03e7f64d6351cc507a4 Subproject commit 82c248ae18ffda0bf161fbb69ddb3fb30cfc1531

View file

@ -9,6 +9,7 @@ class UserData {
required this.username, required this.username,
required this.displayName, required this.displayName,
required this.subscriptionPlan, required this.subscriptionPlan,
required this.currentSetupPage,
}); });
factory UserData.fromJson(Map<String, dynamic> json) => factory UserData.fromJson(Map<String, dynamic> json) =>
_$UserDataFromJson(json); _$UserDataFromJson(json);
@ -136,6 +137,11 @@ class UserData {
// Once a day the anonymous data is collected and send to the server // Once a day the anonymous data is collected and send to the server
DateTime? lastUserStudyDataUpload; DateTime? lastUserStudyDataUpload;
String? currentSetupPage;
@JsonKey(defaultValue: false)
bool skipSetupPages = false;
Map<String, dynamic> toJson() => _$UserDataToJson(this); Map<String, dynamic> toJson() => _$UserDataToJson(this);
} }

View file

@ -12,6 +12,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
username: json['username'] as String, username: json['username'] as String,
displayName: json['displayName'] as String, displayName: json['displayName'] as String,
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free', subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
currentSetupPage: json['currentSetupPage'] as String?,
) )
..avatarSvg = json['avatarSvg'] as String? ..avatarSvg = json['avatarSvg'] as String?
..avatarJson = json['avatarJson'] as String? ..avatarJson = json['avatarJson'] as String?
@ -93,7 +94,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
json['userStudyParticipantsToken'] as String? json['userStudyParticipantsToken'] as String?
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null ..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
? null ? null
: DateTime.parse(json['lastUserStudyDataUpload'] as String); : DateTime.parse(json['lastUserStudyDataUpload'] as String)
..skipSetupPages = json['skipSetupPages'] as bool? ?? false;
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'userId': instance.userId, 'userId': instance.userId,
@ -145,6 +147,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'userStudyParticipantsToken': instance.userStudyParticipantsToken, 'userStudyParticipantsToken': instance.userStudyParticipantsToken,
'lastUserStudyDataUpload': instance.lastUserStudyDataUpload 'lastUserStudyDataUpload': instance.lastUserStudyDataUpload
?.toIso8601String(), ?.toIso8601String(),
'currentSetupPage': instance.currentSetupPage,
'skipSetupPages': instance.skipSetupPages,
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {

View file

@ -33,7 +33,7 @@ import 'package:twonly/src/visual/views/settings/help/contact_us.view.dart';
import 'package:twonly/src/visual/views/settings/help/credits.view.dart'; import 'package:twonly/src/visual/views/settings/help/credits.view.dart';
import 'package:twonly/src/visual/views/settings/help/diagnostics.view.dart'; import 'package:twonly/src/visual/views/settings/help/diagnostics.view.dart';
import 'package:twonly/src/visual/views/settings/help/faq.view.dart'; import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
import 'package:twonly/src/visual/views/settings/help/faq/verification_bade_faq.view.dart'; import 'package:twonly/src/visual/views/settings/help/faq/verification_badge_faq.view.dart';
import 'package:twonly/src/visual/views/settings/help/help.view.dart'; import 'package:twonly/src/visual/views/settings/help/help.view.dart';
import 'package:twonly/src/visual/views/settings/notification.view.dart'; import 'package:twonly/src/visual/views/settings/notification.view.dart';
import 'package:twonly/src/visual/views/settings/privacy.view.dart'; import 'package:twonly/src/visual/views/settings/privacy.view.dart';

View file

@ -290,9 +290,11 @@ Future<List<int>> sha256File(File file) async {
return sha256Sink.events.single.bytes; return sha256Sink.events.single.bytes;
} }
List<TextSpan> formattedText(BuildContext context, String input) { List<TextSpan> formattedText(
// Access the current theme's text color BuildContext context,
// Defaulting to bodyMedium color, but you can use labelLarge, displaySmall, etc. String input, {
Color? boldTextColor,
}) {
final defaultColor = Theme.of(context).colorScheme.onSurface; final defaultColor = Theme.of(context).colorScheme.onSurface;
final regex = RegExp(r'\*(.*?)\*'); final regex = RegExp(r'\*(.*?)\*');
@ -301,7 +303,6 @@ List<TextSpan> formattedText(BuildContext context, String input) {
var lastMatchEnd = 0; var lastMatchEnd = 0;
for (final match in regex.allMatches(input)) { for (final match in regex.allMatches(input)) {
// Add text before the match (Normal style)
if (match.start > lastMatchEnd) { if (match.start > lastMatchEnd) {
spans.add( spans.add(
TextSpan( TextSpan(
@ -311,13 +312,12 @@ List<TextSpan> formattedText(BuildContext context, String input) {
); );
} }
// Add the matched text (Bold style)
spans.add( spans.add(
TextSpan( TextSpan(
text: match.group(1), text: match.group(1),
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: defaultColor, // Ensures bold text also uses the theme color color: boldTextColor ?? defaultColor,
), ),
), ),
); );
@ -325,7 +325,6 @@ List<TextSpan> formattedText(BuildContext context, String input) {
lastMatchEnd = match.end; lastMatchEnd = match.end;
} }
// Add any remaining text after the last match
if (lastMatchEnd < input.length) { if (lastMatchEnd < input.length) {
spans.add( spans.add(
TextSpan( TextSpan(

View file

@ -7,8 +7,8 @@ import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/key_verification.dao.dart'; import 'package:twonly/src/database/daos/key_verification.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
import 'package:twonly/src/visual/elements/svg_icon.element.dart'; import 'package:twonly/src/visual/elements/svg_icon.element.dart';
import 'package:twonly/src/visual/views/settings/help/faq/verification_bade_faq.view.dart';
class VerificationBadgeComp extends StatefulWidget { class VerificationBadgeComp extends StatefulWidget {
const VerificationBadgeComp({ const VerificationBadgeComp({

View 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),
),
),
),
],
),
);
}
}

View file

@ -6,6 +6,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
@ -19,6 +20,7 @@ import 'package:twonly/src/utils/secure_storage.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
class RegisterView extends StatefulWidget { class RegisterView extends StatefulWidget {
const RegisterView({ const RegisterView({
@ -136,7 +138,8 @@ class _RegisterViewState extends State<RegisterView> {
username: username, username: username,
displayName: username, displayName: username,
subscriptionPlan: 'Preview', subscriptionPlan: 'Preview',
)..appVersion = 62; currentSetupPage: SetupPages.profile.name,
)..appVersion = AppState.latestAppVersionId;
await SecureStorage.instance.write( await SecureStorage.instance.write(
key: SecureStorageKeys.userData, key: SecureStorageKeys.userData,

View 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();
}
}
}

View 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,
),
),
),
],
);
}
}

View 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();
}
}

View 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,
),
),
),
],
);
}
}

View file

@ -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),
),
],
),
);
}
}

View file

@ -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,
),
),
),
],
);
}
}

View file

@ -1,6 +1,5 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
@ -8,7 +7,7 @@ import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/backup/common.backup.dart'; import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/decorations/input_text.decoration.dart'; import 'package:twonly/src/visual/views/settings/backup/components/backup_setup.comp.dart';
class SetupBackupView extends StatefulWidget { class SetupBackupView extends StatefulWidget {
const SetupBackupView({ const SetupBackupView({
@ -25,7 +24,6 @@ class SetupBackupView extends StatefulWidget {
} }
class _SetupBackupViewState extends State<SetupBackupView> { class _SetupBackupViewState extends State<SetupBackupView> {
bool obscureText = true;
bool isLoading = false; bool isLoading = false;
final TextEditingController passwordCtrl = TextEditingController(); final TextEditingController passwordCtrl = TextEditingController();
final TextEditingController repeatedPasswordCtrl = TextEditingController(); final TextEditingController repeatedPasswordCtrl = TextEditingController();
@ -53,10 +51,6 @@ class _SetupBackupViewState extends State<SetupBackupView> {
} }
} }
setState(() {
isLoading = true;
});
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
await enableTwonlySafe(passwordCtrl.text); await enableTwonlySafe(passwordCtrl.text);
@ -105,80 +99,28 @@ class _SetupBackupViewState extends State<SetupBackupView> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
Stack( BackupPasswordTextField(
children: [ controller: passwordCtrl,
TextField( labelText: context.lang.password,
controller: passwordCtrl, onChanged: (value) => setState(() {}),
onChanged: (value) {
setState(() {});
},
style: const TextStyle(fontSize: 17),
obscureText: obscureText,
decoration: getInputDecoration(
context,
context.lang.password,
),
),
Positioned(
right: 0,
top: 0,
bottom: 0,
child: IconButton(
onPressed: () {
setState(() {
obscureText = !obscureText;
});
},
icon: FaIcon(
obscureText
? FontAwesomeIcons.eye
: FontAwesomeIcons.eyeSlash,
size: 16,
),
),
),
],
), ),
Padding( PasswordRequirementText(
padding: const EdgeInsetsGeometry.all(5), text: context.lang.backupPasswordRequirement,
child: Text( showError:
context.lang.backupPasswordRequirement, passwordCtrl.text.length < 8 &&
style: TextStyle( passwordCtrl.text.isNotEmpty,
fontSize: 13,
color:
(passwordCtrl.text.length < 8 &&
passwordCtrl.text.isNotEmpty)
? Colors.red
: Colors.transparent,
),
),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
TextField( BackupPasswordTextField(
controller: repeatedPasswordCtrl, controller: repeatedPasswordCtrl,
onChanged: (value) { labelText: context.lang.passwordRepeated,
setState(() {}); onChanged: (value) => setState(() {}),
},
style: const TextStyle(fontSize: 17),
obscureText: true,
decoration: getInputDecoration(
context,
context.lang.passwordRepeated,
),
), ),
Padding( PasswordRequirementText(
padding: const EdgeInsetsGeometry.all(5), text: context.lang.passwordRepeatedNotEqual,
child: Text( showError:
context.lang.passwordRepeatedNotEqual, passwordCtrl.text != repeatedPasswordCtrl.text &&
style: TextStyle( repeatedPasswordCtrl.text.isNotEmpty,
fontSize: 13,
color:
(passwordCtrl.text != repeatedPasswordCtrl.text &&
repeatedPasswordCtrl.text.isNotEmpty)
? Colors.red
: Colors.transparent,
),
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Center( Center(
@ -239,17 +181,3 @@ class _SetupBackupViewState extends State<SetupBackupView> {
); );
} }
} }
Future<bool> isSecurePassword(String password) async {
final badPasswordsStr = await rootBundle.loadString(
'assets/passwords/bad_passwords.txt',
);
final badPasswords = badPasswordsStr.split('\n');
if (badPasswords.contains(password)) {
return false;
}
// Check if the password meets all criteria
return RegExp('[A-Z]').hasMatch(password) &&
RegExp('[a-z]').hasMatch(password) &&
RegExp('[0-9]').hasMatch(password);
}

View file

@ -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,
),
),
);
}
}

View file

@ -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),
),
),
],
),
);
}
}

View file

@ -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),
),
],
),
);
}
}

View file

@ -57,6 +57,7 @@ void main() {
username: 'test_user', username: 'test_user',
displayName: 'Test User', displayName: 'Test User',
subscriptionPlan: 'Free', subscriptionPlan: 'Free',
currentSetupPage: null,
)..appVersion = 62; )..appVersion = 62;
}); });

View file

@ -48,6 +48,7 @@ void main() {
username: 'test_user', username: 'test_user',
displayName: 'Test User', displayName: 'Test User',
subscriptionPlan: 'Free', subscriptionPlan: 'Free',
currentSetupPage: null,
)..appVersion = 100; )..appVersion = 100;
userService.isUserCreated = true; userService.isUserCreated = true;
AppEnvironment.initTesting(); AppEnvironment.initTesting();