This commit is contained in:
otsmr 2025-07-18 14:22:34 +02:00
parent cd9a5bab97
commit afb1806cb1
15 changed files with 143 additions and 202 deletions

11
CHANGELOG.md Normal file
View file

@ -0,0 +1,11 @@
# Changelog
## 0.0.58 (WIP)
- twonly now has a free plan and is now financed by donations and an optional subscription with more features (coming soon)
- Implementing iOS gestures to close images
- Improved the chat messages view, including better citation view and display times.
- Updated onboarding screens and simplified the registration view
- The sender is displayed in the top right corner when a media file is opened.
- Images are now stored as WebP to save storage
- Multiple bug fixes

View file

@ -33,6 +33,10 @@ class _AppState extends State<App> with WidgetsBindingObserver {
setUserPlan();
};
globalCallbackUpdatePlan = (String planId) {
context.read<CustomChangeProvider>().updatePlan(planId);
};
initAsync();
}
@ -86,6 +90,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
void dispose() {
WidgetsBinding.instance.removeObserver(this);
globalCallbackConnectionState = ({required bool isConnected}) {};
globalCallbackUpdatePlan = (String planId) {};
super.dispose();
}

View file

@ -19,6 +19,7 @@ void Function({required bool isConnected}) globalCallbackConnectionState = ({
required bool isConnected,
}) {};
void Function() globalCallbackAppIsOutdated = () {};
void Function(String planId) globalCallbackUpdatePlan = (String planId) {};
bool globalIsAppInBackground = true;
int globalBestFriendUserId = -1;

View file

@ -46,7 +46,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
'my_avatar_counter', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: Constant(0));
defaultValue: const Constant(0));
static const VerificationMeta _acceptedMeta =
const VerificationMeta('accepted');
@override
@ -56,7 +56,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("accepted" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
static const VerificationMeta _requestedMeta =
const VerificationMeta('requested');
@override
@ -66,7 +66,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("requested" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
static const VerificationMeta _blockedMeta =
const VerificationMeta('blocked');
@override
@ -76,7 +76,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("blocked" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
static const VerificationMeta _verifiedMeta =
const VerificationMeta('verified');
@override
@ -86,7 +86,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("verified" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
static const VerificationMeta _archivedMeta =
const VerificationMeta('archived');
@override
@ -96,7 +96,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned');
@override
late final GeneratedColumn<bool> pinned = GeneratedColumn<bool>(
@ -105,7 +105,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
static const VerificationMeta _deletedMeta =
const VerificationMeta('deleted');
@override
@ -115,7 +115,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("deleted" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
static const VerificationMeta _alsoBestFriendMeta =
const VerificationMeta('alsoBestFriend');
@override
@ -125,7 +125,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("also_best_friend" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
static const VerificationMeta _deleteMessagesAfterXMinutesMeta =
const VerificationMeta('deleteMessagesAfterXMinutes');
@override
@ -134,7 +134,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
'delete_messages_after_x_minutes', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: Constant(60 * 24));
defaultValue: const Constant(60 * 24));
static const VerificationMeta _createdAtMeta =
const VerificationMeta('createdAt');
@override
@ -150,7 +150,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
'total_media_counter', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: Constant(0));
defaultValue: const Constant(0));
static const VerificationMeta _lastMessageSendMeta =
const VerificationMeta('lastMessageSend');
@override
@ -190,7 +190,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
'flame_counter', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: Constant(0));
defaultValue: const Constant(0));
@override
List<GeneratedColumn> get $columns => [
userId,
@ -1160,7 +1160,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("acknowledge_by_user" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
static const VerificationMeta _mediaStoredMeta =
const VerificationMeta('mediaStored');
@override
@ -1170,7 +1170,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("media_stored" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
@override
late final GeneratedColumnWithTypeConverter<DownloadState, int>
downloadState = GeneratedColumn<int>('download_state', aliasedName, false,
@ -1187,7 +1187,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("acknowledge_by_server" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
static const VerificationMeta _errorWhileSendingMeta =
const VerificationMeta('errorWhileSending');
@override
@ -1197,7 +1197,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("error_while_sending" IN (0, 1))'),
defaultValue: Constant(false));
defaultValue: const Constant(false));
@override
late final GeneratedColumnWithTypeConverter<MediaRetransmitting, String>
mediaRetransmissionState = GeneratedColumn<String>(
@ -2099,7 +2099,7 @@ class $MediaUploadsTable extends MediaUploads
static JsonTypeConverter2<UploadState, String, String> $converterstate =
const EnumNameConverter<UploadState>(UploadState.values);
static JsonTypeConverter2<MediaUploadMetadata, String, Map<String, Object?>>
$convertermetadata = MediaUploadMetadataConverter();
$convertermetadata = const MediaUploadMetadataConverter();
static JsonTypeConverter2<MediaUploadMetadata?, String?,
Map<String, Object?>?> $convertermetadatan =
JsonTypeConverter2.asNullable($convertermetadata);
@ -2108,7 +2108,7 @@ class $MediaUploadsTable extends MediaUploads
static TypeConverter<List<int>?, String?> $convertermessageIdsn =
NullAwareTypeConverter.wrap($convertermessageIds);
static JsonTypeConverter2<MediaEncryptionData, String, Map<String, Object?>>
$converterencryptionData = MediaEncryptionDataConverter();
$converterencryptionData = const MediaEncryptionDataConverter();
static JsonTypeConverter2<MediaEncryptionData?, String?,
Map<String, Object?>?> $converterencryptionDatan =
JsonTypeConverter2.asNullable($converterencryptionData);

View file

@ -213,15 +213,16 @@
"errorPlanUpgradeNotYearly": "Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.",
"proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"proFeature2": "1 zusätzlicher Plus Benutzer",
"proFeature3": "3 zusätzliche kostenlose Benutzer",
"proFeature3": "Zusatzfunktionen (coming-soon)",
"proFeature4": "Cloud-Backup verschlüsselt (coming-soon)",
"year": "year",
"month": "month",
"familyFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"familyFeature1": "✓ Alles von Pro",
"familyFeature2": "4 zusätzliche Plus Benutzer",
"familyFeature3": "4 zusätzliche kostenlose Benutzer",
"redeemUserInviteCode": "Oder löse einen twonly-Code ein.",
"freeFeature1": "3 Medien-Datei-Uploads pro Tag",
"freeFeature1": "10 Medien-Datei-Uploads pro Tag",
"plusFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"plusFeature2": "Zusatzfunktionen (coming-soon)",
"transactionHistory": "Transaktionshistorie",
"currentBalance": "Dein Guthaben",
"manageAdditionalUsers": "Zusätzliche Benutzer verwalten",
@ -329,7 +330,5 @@
"appOutdatedBtn": "Jetzt aktualisieren.",
"doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.",
"retransmissionRequested": "Wird erneut versucht.",
"testPaymentMethod": "twonly befindet sich derzeit in einer Testphase und kann nur mit einem Einladungscode vollständig genutzt werden. Es gibt derzeit keine Zahlungsmethode, um Ihr twonly-Guthaben aufzuladen!",
"testingAccountTitle": "Tester-Zugang",
"testingAccountBody": "Danke für dein Interesse! Wir werden deine Anfrage prüfen und den Plan so schnell wie möglich aktivieren. Da wir uns jedoch noch in einer Testphase befinden, ist die Anzahl der Tester-Konten begrenzt. Wir werden dich jedoch benachrichtigen, sobald dir ein Platz zugewiesen wurde."
"testPaymentMethod": "Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!"
}

View file

@ -367,15 +367,16 @@
"month": "month",
"proFeature1": "✓ Unlimited media file uploads",
"proFeature2": "1 additional Plus user",
"proFeature3": "3 additional Free users",
"familyFeature1": "✓ Unlimited media file uploads",
"proFeature3": "Cloud-Backup encrypted (coming-soon)",
"proFeature4": "Additional features (coming-soon)",
"familyFeature1": "✓ All from Pro",
"familyFeature2": "4 additional Plus users",
"familyFeature3": "4 additional Free users",
"redeemUserInviteCode": "Or redeem a twonly-Code.",
"redeemUserInviteCodeTitle": "Redeem twonly-Code",
"redeemUserInviteCodeSuccess": "Your plan has been successfully adjusted.",
"freeFeature1": "3 Media file uploads per day",
"freeFeature1": "10 Media file uploads per day",
"plusFeature1": "✓ Unlimited media file uploads",
"plusFeature2": "Additional features (coming-soon)",
"transactionHistory": "Your transaction history",
"manageSubscription": "Manage your subscription",
"nextPayment": "Next payment",
@ -410,7 +411,6 @@
"twonlyCredit": "twonly-Credit",
"notEnoughCredit": "You do not have enough credit!",
"chargeCredit": "Charge credit",
"chargeCredit": "Charge credit",
"autoRenewal": "Auto renewal",
"autoRenewalDesc": "You can change this at any time.",
"autoRenewalLongDesc": "When your subscription expires, you will automatically be downgraded to the Preview plan. If you activate the automatic renewal, please make sure that you have enough credit for the automatic renewal. We will notify you in good time before the automatic renewal.",
@ -486,7 +486,5 @@
"appOutdatedBtn": "Update Now",
"doubleClickToReopen": "Double-click\nto open again",
"retransmissionRequested": "Retransmission requested",
"testPaymentMethod": "twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!",
"testingAccountTitle": "Tester account activation",
"testingAccountBody": "Thank you for your interest! We will check your request and activate the plan as soon as possible. However, as we are currently still in a test phase, the number of tester accounts is limited. However, we will notify you as soon as you have been allocated a place."
"testPaymentMethod": "Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!"
}

View file

@ -1307,13 +1307,19 @@ abstract class AppLocalizations {
/// No description provided for @proFeature3.
///
/// In en, this message translates to:
/// **'3 additional Free users'**
/// **'Cloud-Backup encrypted (coming-soon)'**
String get proFeature3;
/// No description provided for @proFeature4.
///
/// In en, this message translates to:
/// **'Additional features (coming-soon)'**
String get proFeature4;
/// No description provided for @familyFeature1.
///
/// In en, this message translates to:
/// **'✓ Unlimited media file uploads'**
/// **'All from Pro'**
String get familyFeature1;
/// No description provided for @familyFeature2.
@ -1322,12 +1328,6 @@ abstract class AppLocalizations {
/// **'4 additional Plus users'**
String get familyFeature2;
/// No description provided for @familyFeature3.
///
/// In en, this message translates to:
/// **'4 additional Free users'**
String get familyFeature3;
/// No description provided for @redeemUserInviteCode.
///
/// In en, this message translates to:
@ -1349,7 +1349,7 @@ abstract class AppLocalizations {
/// No description provided for @freeFeature1.
///
/// In en, this message translates to:
/// **'3 Media file uploads per day'**
/// **'10 Media file uploads per day'**
String get freeFeature1;
/// No description provided for @plusFeature1.
@ -1358,6 +1358,12 @@ abstract class AppLocalizations {
/// **'✓ Unlimited media file uploads'**
String get plusFeature1;
/// No description provided for @plusFeature2.
///
/// In en, this message translates to:
/// **'Additional features (coming-soon)'**
String get plusFeature2;
/// No description provided for @transactionHistory.
///
/// In en, this message translates to:
@ -2015,20 +2021,8 @@ abstract class AppLocalizations {
/// No description provided for @testPaymentMethod.
///
/// In en, this message translates to:
/// **'twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!'**
/// **'Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!'**
String get testPaymentMethod;
/// No description provided for @testingAccountTitle.
///
/// In en, this message translates to:
/// **'Tester account activation'**
String get testingAccountTitle;
/// No description provided for @testingAccountBody.
///
/// In en, this message translates to:
/// **'Thank you for your interest! We will check your request and activate the plan as soon as possible. However, as we are currently still in a test phase, the number of tester accounts is limited. However, we will notify you as soon as you have been allocated a place.'**
String get testingAccountBody;
}
class _AppLocalizationsDelegate

View file

@ -684,17 +684,17 @@ class AppLocalizationsDe extends AppLocalizations {
String get proFeature2 => '1 zusätzlicher Plus Benutzer';
@override
String get proFeature3 => '3 zusätzliche kostenlose Benutzer';
String get proFeature3 => 'Zusatzfunktionen (coming-soon)';
@override
String get familyFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
String get proFeature4 => 'Cloud-Backup verschlüsselt (coming-soon)';
@override
String get familyFeature1 => '✓ Alles von Pro';
@override
String get familyFeature2 => '4 zusätzliche Plus Benutzer';
@override
String get familyFeature3 => '4 zusätzliche kostenlose Benutzer';
@override
String get redeemUserInviteCode => 'Oder löse einen twonly-Code ein.';
@ -706,11 +706,14 @@ class AppLocalizationsDe extends AppLocalizations {
'Dein Plan wurde erfolgreich angepasst.';
@override
String get freeFeature1 => '3 Medien-Datei-Uploads pro Tag';
String get freeFeature1 => '10 Medien-Datei-Uploads pro Tag';
@override
String get plusFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
@override
String get plusFeature2 => 'Zusatzfunktionen (coming-soon)';
@override
String get transactionHistory => 'Transaktionshistorie';
@ -1069,12 +1072,5 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get testPaymentMethod =>
'twonly befindet sich derzeit in einer Testphase und kann nur mit einem Einladungscode vollständig genutzt werden. Es gibt derzeit keine Zahlungsmethode, um Ihr twonly-Guthaben aufzuladen!';
@override
String get testingAccountTitle => 'Tester-Zugang';
@override
String get testingAccountBody =>
'Danke für dein Interesse! Wir werden deine Anfrage prüfen und den Plan so schnell wie möglich aktivieren. Da wir uns jedoch noch in einer Testphase befinden, ist die Anzahl der Tester-Konten begrenzt. Wir werden dich jedoch benachrichtigen, sobald dir ein Platz zugewiesen wurde.';
'Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!';
}

View file

@ -679,17 +679,17 @@ class AppLocalizationsEn extends AppLocalizations {
String get proFeature2 => '1 additional Plus user';
@override
String get proFeature3 => '3 additional Free users';
String get proFeature3 => 'Cloud-Backup encrypted (coming-soon)';
@override
String get familyFeature1 => '✓ Unlimited media file uploads';
String get proFeature4 => 'Additional features (coming-soon)';
@override
String get familyFeature1 => '✓ All from Pro';
@override
String get familyFeature2 => '4 additional Plus users';
@override
String get familyFeature3 => '4 additional Free users';
@override
String get redeemUserInviteCode => 'Or redeem a twonly-Code.';
@ -701,11 +701,14 @@ class AppLocalizationsEn extends AppLocalizations {
'Your plan has been successfully adjusted.';
@override
String get freeFeature1 => '3 Media file uploads per day';
String get freeFeature1 => '10 Media file uploads per day';
@override
String get plusFeature1 => '✓ Unlimited media file uploads';
@override
String get plusFeature2 => 'Additional features (coming-soon)';
@override
String get transactionHistory => 'Your transaction history';
@ -1063,12 +1066,5 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get testPaymentMethod =>
'twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!';
@override
String get testingAccountTitle => 'Tester account activation';
@override
String get testingAccountBody =>
'Thank you for your interest! We will check your request and activate the plan as soon as possible. However, as we are currently still in a test phase, the number of tester accounts is limited. However, we will notify you as soon as you have been allocated a place.';
'Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!';
}

View file

@ -67,9 +67,6 @@ class UserData {
DateTime? signalLastSignedPreKeyUpdated;
@JsonKey(defaultValue: false)
bool requestedTesterAccount = false;
// -- Custom DATA --
@JsonKey(defaultValue: 100_000)
@ -78,6 +75,8 @@ class UserData {
@JsonKey(defaultValue: 100_000)
int currentSignedPreKeyIndexStart = 100_000;
List<int>? lastChangeLogHash;
// --- BACKUP ---
DateTime? nextTimeToShowBackupNotice;

View file

@ -49,12 +49,13 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
json['signalLastSignedPreKeyUpdated'] == null
? null
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..requestedTesterAccount =
json['requestedTesterAccount'] as bool? ?? false
..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart =
(json['currentSignedPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..lastChangeLogHash = (json['lastChangeLogHash'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList()
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
? null
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
@ -91,9 +92,9 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'myBestFriendContactId': instance.myBestFriendContactId,
'signalLastSignedPreKeyUpdated':
instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
'requestedTesterAccount': instance.requestedTesterAccount,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash,
'nextTimeToShowBackupNotice':
instance.nextTimeToShowBackupNotice?.toIso8601String(),
'backupServer': instance.backupServer,

View file

@ -366,6 +366,7 @@ class ApiService {
user.subscriptionPlan = authenticated.plan;
return user;
});
globalCallbackUpdatePlan(authenticated.plan);
}
Log.info('websocket is authenticated');
unawaited(onAuthenticated());

View file

@ -88,32 +88,34 @@ class _ChatListViewState extends State<ChatListView> {
@override
Widget build(BuildContext context) {
final isConnected = context.watch<CustomChangeProvider>().isConnected;
final planId = context.watch<CustomChangeProvider>().plan;
return Scaffold(
appBar: AppBar(
title: Row(children: [
const Text('twonly '),
GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return const SubscriptionView();
}));
},
child: Container(
decoration: BoxDecoration(
color: context.color.primary,
borderRadius: BorderRadius.circular(15),
),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
child: Text(
context.watch<CustomChangeProvider>().plan,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: isDarkMode(context) ? Colors.black : Colors.white,
if (planId != 'Free')
GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return const SubscriptionView();
}));
},
child: Container(
decoration: BoxDecoration(
color: context.color.primary,
borderRadius: BorderRadius.circular(15),
),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
child: Text(
planId,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: isDarkMode(context) ? Colors.black : Colors.white,
),
),
),
),
),
]),
actions: [
if (showFeedbackShortcut)

View file

@ -64,7 +64,7 @@ class _ManageSubscriptionViewState extends State<ManageSubscriptionView> {
final planId = context.read<CustomChangeProvider>().plan;
final myLocale = Localizations.localeOf(context);
final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS;
final isAdditionalUser = planId == 'Free' || planId == 'Plus';
final isPayingUser = planId == 'Family' || planId == 'Pro';
return Scaffold(
appBar: AppBar(
title: Text(context.lang.manageSubscription),
@ -72,14 +72,14 @@ class _ManageSubscriptionViewState extends State<ManageSubscriptionView> {
body: ListView(
children: [
PlanCard(planId: planId, paidMonthly: paidMonthly),
if (!isAdditionalUser) const SizedBox(height: 20),
if (widget.nextPayment != null && !isAdditionalUser)
if (isPayingUser) const SizedBox(height: 20),
if (widget.nextPayment != null && isPayingUser)
ListTile(
title: Text(
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(widget.nextPayment!)}',
),
),
if (autoRenewal != null && !isAdditionalUser)
if (autoRenewal != null && isPayingUser)
ListTile(
title: Text(context.lang.autoRenewal),
subtitle: Text(

View file

@ -2,7 +2,6 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
@ -13,7 +12,6 @@ import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/settings/subscription/additional_users.view.dart';
import 'package:twonly/src/views/settings/subscription/checkout.view.dart';
@ -129,59 +127,21 @@ class _SubscriptionViewState extends State<SubscriptionView> {
additionalOwnerName = ownerId.toString();
}
}
final user = await getUser();
if (user != null) {
testerRequested = user.requestedTesterAccount;
// if (kDebugMode) {
// testerRequested = false;
// }
}
setState(() {});
}
Future<void> _submitTesterRequest() async {
final user = await getUser();
if (user == null) return;
final response = await http.post(
Uri.parse('https://twonly.theconnectapp.de/subscribe.twonly.php'),
headers: <String, String>{
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
'feedback': '[TESTER REQUEST] ${user.username} ${user.userId}',
},
);
if (!mounted) return;
if (response.statusCode == 200) {
// Handle successful response
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Your request has been submitted.')),
);
await updateUserdata((u) {
u.requestedTesterAccount = true;
return u;
});
await initAsync();
} else {
// Handle error response
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to submit your request.')),
);
}
}
@override
Widget build(BuildContext context) {
final myLocale = Localizations.localeOf(context);
String? formattedBalance;
DateTime? nextPayment;
final currentPlan = context.read<CustomChangeProvider>().plan;
final isAdditionalUser = currentPlan == 'Free' || currentPlan == 'Plus';
final isPayingUser = currentPlan == 'Family' || currentPlan == 'Pro';
if (ballance != null) {
final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch(
ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000);
if (!isAdditionalUser) {
if (isPayingUser) {
nextPayment = lastPaymentDateTime
.add(Duration(days: ballance!.paymentPeriodDays.toInt()));
}
@ -262,20 +222,6 @@ class _SubscriptionViewState extends State<SubscriptionView> {
),
),
),
if (currentPlan != 'Tester' && !testerRequested)
PlanCard(
planId: 'Tester',
onTap: () async {
final activate = await showAlertDialog(
context,
context.lang.testingAccountTitle,
context.lang.testingAccountBody,
);
if (activate) {
await _submitTesterRequest();
}
},
),
if (currentPlan != 'Family' && currentPlan != 'Pro')
PlanCard(
planId: 'Pro',
@ -307,7 +253,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
await initAsync();
},
),
if (currentPlan == 'Preview' || currentPlan == 'Free') ...[
if (!isPayingUser) ...[
const SizedBox(height: 10),
Center(
child: Padding(
@ -320,14 +266,6 @@ class _SubscriptionViewState extends State<SubscriptionView> {
),
),
const SizedBox(height: 10),
if (currentPlan != 'Free')
PlanCard(
planId: 'Free',
onTap: () async {
await redeemUserInviteCode(context, 'Free');
await initAsync();
},
),
PlanCard(
planId: 'Plus',
onTap: () async {
@ -338,25 +276,24 @@ class _SubscriptionViewState extends State<SubscriptionView> {
],
const SizedBox(height: 10),
if (currentPlan != 'Family') const Divider(),
if (currentPlan != 'Preview')
BetterListTile(
icon: FontAwesomeIcons.gears,
text: context.lang.manageSubscription,
subtitle: (nextPayment != null)
? Text(
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(nextPayment)}')
: null,
onTap: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) {
return ManageSubscriptionView(
ballance: ballance,
nextPayment: nextPayment,
);
}));
await initAsync();
},
),
BetterListTile(
icon: FontAwesomeIcons.gears,
text: context.lang.manageSubscription,
subtitle: (nextPayment != null)
? Text(
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(nextPayment)}')
: null,
onTap: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) {
return ManageSubscriptionView(
ballance: ballance,
nextPayment: nextPayment,
);
}));
await initAsync();
},
),
BetterListTile(
icon: FontAwesomeIcons.moneyBillTransfer,
text: context.lang.transactionHistory,
@ -373,7 +310,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
}));
},
),
if (!isAdditionalUser)
if (isPayingUser)
BetterListTile(
icon: FontAwesomeIcons.userPlus,
text: context.lang.manageAdditionalUsers,
@ -434,24 +371,25 @@ class PlanCard extends StatelessWidget {
final yearlyPrice = getPlanPrice(planId, paidMonthly: false);
final monthlyPrice = getPlanPrice(planId, paidMonthly: true);
var features = <String>[];
final isPayingUser = planId == 'Family' || planId == 'Pro';
switch (planId) {
case 'Free':
features = [context.lang.freeFeature1];
case 'Plus':
features = [context.lang.plusFeature1];
features = [context.lang.plusFeature1, context.lang.plusFeature2];
case 'Tester':
case 'Pro':
features = [
context.lang.proFeature1,
context.lang.proFeature2,
context.lang.proFeature3
context.lang.proFeature3,
context.lang.proFeature4,
];
case 'Family':
features = [
context.lang.familyFeature1,
context.lang.familyFeature2,
context.lang.familyFeature3
];
default:
}
@ -481,7 +419,7 @@ class PlanCard extends StatelessWidget {
),
),
if (yearlyPrice != 0) const SizedBox(height: 10),
if (yearlyPrice != 0 && paidMonthly == null)
if (isPayingUser)
Column(
children: [
if (paidMonthly == null || paidMonthly!)
@ -504,7 +442,7 @@ class PlanCard extends StatelessWidget {
),
],
),
if (paidMonthly != null)
if (isPayingUser && paidMonthly != null)
Text(
(paidMonthly!)
? '${localePrizing(context, monthlyPrice)}/${context.lang.month}'