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

View file

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

View file

@ -46,7 +46,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
'my_avatar_counter', aliasedName, false, 'my_avatar_counter', aliasedName, false,
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: Constant(0)); defaultValue: const Constant(0));
static const VerificationMeta _acceptedMeta = static const VerificationMeta _acceptedMeta =
const VerificationMeta('accepted'); const VerificationMeta('accepted');
@override @override
@ -56,7 +56,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("accepted" IN (0, 1))'), GeneratedColumn.constraintIsAlways('CHECK ("accepted" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _requestedMeta = static const VerificationMeta _requestedMeta =
const VerificationMeta('requested'); const VerificationMeta('requested');
@override @override
@ -66,7 +66,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("requested" IN (0, 1))'), GeneratedColumn.constraintIsAlways('CHECK ("requested" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _blockedMeta = static const VerificationMeta _blockedMeta =
const VerificationMeta('blocked'); const VerificationMeta('blocked');
@override @override
@ -76,7 +76,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("blocked" IN (0, 1))'), GeneratedColumn.constraintIsAlways('CHECK ("blocked" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _verifiedMeta = static const VerificationMeta _verifiedMeta =
const VerificationMeta('verified'); const VerificationMeta('verified');
@override @override
@ -86,7 +86,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("verified" IN (0, 1))'), GeneratedColumn.constraintIsAlways('CHECK ("verified" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _archivedMeta = static const VerificationMeta _archivedMeta =
const VerificationMeta('archived'); const VerificationMeta('archived');
@override @override
@ -96,7 +96,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned');
@override @override
late final GeneratedColumn<bool> pinned = GeneratedColumn<bool>( late final GeneratedColumn<bool> pinned = GeneratedColumn<bool>(
@ -105,7 +105,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _deletedMeta = static const VerificationMeta _deletedMeta =
const VerificationMeta('deleted'); const VerificationMeta('deleted');
@override @override
@ -115,7 +115,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("deleted" IN (0, 1))'), GeneratedColumn.constraintIsAlways('CHECK ("deleted" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _alsoBestFriendMeta = static const VerificationMeta _alsoBestFriendMeta =
const VerificationMeta('alsoBestFriend'); const VerificationMeta('alsoBestFriend');
@override @override
@ -125,7 +125,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("also_best_friend" IN (0, 1))'), 'CHECK ("also_best_friend" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _deleteMessagesAfterXMinutesMeta = static const VerificationMeta _deleteMessagesAfterXMinutesMeta =
const VerificationMeta('deleteMessagesAfterXMinutes'); const VerificationMeta('deleteMessagesAfterXMinutes');
@override @override
@ -134,7 +134,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
'delete_messages_after_x_minutes', aliasedName, false, 'delete_messages_after_x_minutes', aliasedName, false,
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: Constant(60 * 24)); defaultValue: const Constant(60 * 24));
static const VerificationMeta _createdAtMeta = static const VerificationMeta _createdAtMeta =
const VerificationMeta('createdAt'); const VerificationMeta('createdAt');
@override @override
@ -150,7 +150,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
'total_media_counter', aliasedName, false, 'total_media_counter', aliasedName, false,
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: Constant(0)); defaultValue: const Constant(0));
static const VerificationMeta _lastMessageSendMeta = static const VerificationMeta _lastMessageSendMeta =
const VerificationMeta('lastMessageSend'); const VerificationMeta('lastMessageSend');
@override @override
@ -190,7 +190,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> {
'flame_counter', aliasedName, false, 'flame_counter', aliasedName, false,
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: Constant(0)); defaultValue: const Constant(0));
@override @override
List<GeneratedColumn> get $columns => [ List<GeneratedColumn> get $columns => [
userId, userId,
@ -1160,7 +1160,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("acknowledge_by_user" IN (0, 1))'), 'CHECK ("acknowledge_by_user" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _mediaStoredMeta = static const VerificationMeta _mediaStoredMeta =
const VerificationMeta('mediaStored'); const VerificationMeta('mediaStored');
@override @override
@ -1170,7 +1170,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("media_stored" IN (0, 1))'), 'CHECK ("media_stored" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
@override @override
late final GeneratedColumnWithTypeConverter<DownloadState, int> late final GeneratedColumnWithTypeConverter<DownloadState, int>
downloadState = GeneratedColumn<int>('download_state', aliasedName, false, downloadState = GeneratedColumn<int>('download_state', aliasedName, false,
@ -1187,7 +1187,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("acknowledge_by_server" IN (0, 1))'), 'CHECK ("acknowledge_by_server" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _errorWhileSendingMeta = static const VerificationMeta _errorWhileSendingMeta =
const VerificationMeta('errorWhileSending'); const VerificationMeta('errorWhileSending');
@override @override
@ -1197,7 +1197,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("error_while_sending" IN (0, 1))'), 'CHECK ("error_while_sending" IN (0, 1))'),
defaultValue: Constant(false)); defaultValue: const Constant(false));
@override @override
late final GeneratedColumnWithTypeConverter<MediaRetransmitting, String> late final GeneratedColumnWithTypeConverter<MediaRetransmitting, String>
mediaRetransmissionState = GeneratedColumn<String>( mediaRetransmissionState = GeneratedColumn<String>(
@ -2099,7 +2099,7 @@ class $MediaUploadsTable extends MediaUploads
static JsonTypeConverter2<UploadState, String, String> $converterstate = static JsonTypeConverter2<UploadState, String, String> $converterstate =
const EnumNameConverter<UploadState>(UploadState.values); const EnumNameConverter<UploadState>(UploadState.values);
static JsonTypeConverter2<MediaUploadMetadata, String, Map<String, Object?>> static JsonTypeConverter2<MediaUploadMetadata, String, Map<String, Object?>>
$convertermetadata = MediaUploadMetadataConverter(); $convertermetadata = const MediaUploadMetadataConverter();
static JsonTypeConverter2<MediaUploadMetadata?, String?, static JsonTypeConverter2<MediaUploadMetadata?, String?,
Map<String, Object?>?> $convertermetadatan = Map<String, Object?>?> $convertermetadatan =
JsonTypeConverter2.asNullable($convertermetadata); JsonTypeConverter2.asNullable($convertermetadata);
@ -2108,7 +2108,7 @@ class $MediaUploadsTable extends MediaUploads
static TypeConverter<List<int>?, String?> $convertermessageIdsn = static TypeConverter<List<int>?, String?> $convertermessageIdsn =
NullAwareTypeConverter.wrap($convertermessageIds); NullAwareTypeConverter.wrap($convertermessageIds);
static JsonTypeConverter2<MediaEncryptionData, String, Map<String, Object?>> static JsonTypeConverter2<MediaEncryptionData, String, Map<String, Object?>>
$converterencryptionData = MediaEncryptionDataConverter(); $converterencryptionData = const MediaEncryptionDataConverter();
static JsonTypeConverter2<MediaEncryptionData?, String?, static JsonTypeConverter2<MediaEncryptionData?, String?,
Map<String, Object?>?> $converterencryptionDatan = Map<String, Object?>?> $converterencryptionDatan =
JsonTypeConverter2.asNullable($converterencryptionData); 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.", "errorPlanUpgradeNotYearly": "Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.",
"proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads", "proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"proFeature2": "1 zusätzlicher Plus Benutzer", "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", "year": "year",
"month": "month", "month": "month",
"familyFeature1": "✓ Unbegrenzte Medien-Datei-Uploads", "familyFeature1": "✓ Alles von Pro",
"familyFeature2": "4 zusätzliche Plus Benutzer", "familyFeature2": "4 zusätzliche Plus Benutzer",
"familyFeature3": "4 zusätzliche kostenlose Benutzer",
"redeemUserInviteCode": "Oder löse einen twonly-Code ein.", "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", "plusFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"plusFeature2": "Zusatzfunktionen (coming-soon)",
"transactionHistory": "Transaktionshistorie", "transactionHistory": "Transaktionshistorie",
"currentBalance": "Dein Guthaben", "currentBalance": "Dein Guthaben",
"manageAdditionalUsers": "Zusätzliche Benutzer verwalten", "manageAdditionalUsers": "Zusätzliche Benutzer verwalten",
@ -329,7 +330,5 @@
"appOutdatedBtn": "Jetzt aktualisieren.", "appOutdatedBtn": "Jetzt aktualisieren.",
"doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.", "doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.",
"retransmissionRequested": "Wird erneut versucht.", "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!", "testPaymentMethod": "Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!"
"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."
} }

View file

@ -367,15 +367,16 @@
"month": "month", "month": "month",
"proFeature1": "✓ Unlimited media file uploads", "proFeature1": "✓ Unlimited media file uploads",
"proFeature2": "1 additional Plus user", "proFeature2": "1 additional Plus user",
"proFeature3": "3 additional Free users", "proFeature3": "Cloud-Backup encrypted (coming-soon)",
"familyFeature1": "✓ Unlimited media file uploads", "proFeature4": "Additional features (coming-soon)",
"familyFeature1": "✓ All from Pro",
"familyFeature2": "4 additional Plus users", "familyFeature2": "4 additional Plus users",
"familyFeature3": "4 additional Free users",
"redeemUserInviteCode": "Or redeem a twonly-Code.", "redeemUserInviteCode": "Or redeem a twonly-Code.",
"redeemUserInviteCodeTitle": "Redeem twonly-Code", "redeemUserInviteCodeTitle": "Redeem twonly-Code",
"redeemUserInviteCodeSuccess": "Your plan has been successfully adjusted.", "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", "plusFeature1": "✓ Unlimited media file uploads",
"plusFeature2": "Additional features (coming-soon)",
"transactionHistory": "Your transaction history", "transactionHistory": "Your transaction history",
"manageSubscription": "Manage your subscription", "manageSubscription": "Manage your subscription",
"nextPayment": "Next payment", "nextPayment": "Next payment",
@ -410,7 +411,6 @@
"twonlyCredit": "twonly-Credit", "twonlyCredit": "twonly-Credit",
"notEnoughCredit": "You do not have enough credit!", "notEnoughCredit": "You do not have enough credit!",
"chargeCredit": "Charge credit", "chargeCredit": "Charge credit",
"chargeCredit": "Charge credit",
"autoRenewal": "Auto renewal", "autoRenewal": "Auto renewal",
"autoRenewalDesc": "You can change this at any time.", "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.", "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", "appOutdatedBtn": "Update Now",
"doubleClickToReopen": "Double-click\nto open again", "doubleClickToReopen": "Double-click\nto open again",
"retransmissionRequested": "Retransmission requested", "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!", "testPaymentMethod": "Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!"
"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."
} }

View file

@ -1307,13 +1307,19 @@ abstract class AppLocalizations {
/// No description provided for @proFeature3. /// No description provided for @proFeature3.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'3 additional Free users'** /// **'Cloud-Backup encrypted (coming-soon)'**
String get proFeature3; 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. /// No description provided for @familyFeature1.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'✓ Unlimited media file uploads'** /// **'All from Pro'**
String get familyFeature1; String get familyFeature1;
/// No description provided for @familyFeature2. /// No description provided for @familyFeature2.
@ -1322,12 +1328,6 @@ abstract class AppLocalizations {
/// **'4 additional Plus users'** /// **'4 additional Plus users'**
String get familyFeature2; 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. /// No description provided for @redeemUserInviteCode.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1349,7 +1349,7 @@ abstract class AppLocalizations {
/// No description provided for @freeFeature1. /// No description provided for @freeFeature1.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'3 Media file uploads per day'** /// **'10 Media file uploads per day'**
String get freeFeature1; String get freeFeature1;
/// No description provided for @plusFeature1. /// No description provided for @plusFeature1.
@ -1358,6 +1358,12 @@ abstract class AppLocalizations {
/// **'✓ Unlimited media file uploads'** /// **'✓ Unlimited media file uploads'**
String get plusFeature1; 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. /// No description provided for @transactionHistory.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2015,20 +2021,8 @@ abstract class AppLocalizations {
/// No description provided for @testPaymentMethod. /// No description provided for @testPaymentMethod.
/// ///
/// In en, this message translates to: /// 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; 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 class _AppLocalizationsDelegate

View file

@ -684,17 +684,17 @@ class AppLocalizationsDe extends AppLocalizations {
String get proFeature2 => '1 zusätzlicher Plus Benutzer'; String get proFeature2 => '1 zusätzlicher Plus Benutzer';
@override @override
String get proFeature3 => '3 zusätzliche kostenlose Benutzer'; String get proFeature3 => 'Zusatzfunktionen (coming-soon)';
@override @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 @override
String get familyFeature2 => '4 zusätzliche Plus Benutzer'; String get familyFeature2 => '4 zusätzliche Plus Benutzer';
@override
String get familyFeature3 => '4 zusätzliche kostenlose Benutzer';
@override @override
String get redeemUserInviteCode => 'Oder löse einen twonly-Code ein.'; String get redeemUserInviteCode => 'Oder löse einen twonly-Code ein.';
@ -706,11 +706,14 @@ class AppLocalizationsDe extends AppLocalizations {
'Dein Plan wurde erfolgreich angepasst.'; 'Dein Plan wurde erfolgreich angepasst.';
@override @override
String get freeFeature1 => '3 Medien-Datei-Uploads pro Tag'; String get freeFeature1 => '10 Medien-Datei-Uploads pro Tag';
@override @override
String get plusFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; String get plusFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
@override
String get plusFeature2 => 'Zusatzfunktionen (coming-soon)';
@override @override
String get transactionHistory => 'Transaktionshistorie'; String get transactionHistory => 'Transaktionshistorie';
@ -1069,12 +1072,5 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get testPaymentMethod => 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!'; 'Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!';
@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.';
} }

View file

@ -679,17 +679,17 @@ class AppLocalizationsEn extends AppLocalizations {
String get proFeature2 => '1 additional Plus user'; String get proFeature2 => '1 additional Plus user';
@override @override
String get proFeature3 => '3 additional Free users'; String get proFeature3 => 'Cloud-Backup encrypted (coming-soon)';
@override @override
String get familyFeature1 => '✓ Unlimited media file uploads'; String get proFeature4 => 'Additional features (coming-soon)';
@override
String get familyFeature1 => '✓ All from Pro';
@override @override
String get familyFeature2 => '4 additional Plus users'; String get familyFeature2 => '4 additional Plus users';
@override
String get familyFeature3 => '4 additional Free users';
@override @override
String get redeemUserInviteCode => 'Or redeem a twonly-Code.'; String get redeemUserInviteCode => 'Or redeem a twonly-Code.';
@ -701,11 +701,14 @@ class AppLocalizationsEn extends AppLocalizations {
'Your plan has been successfully adjusted.'; 'Your plan has been successfully adjusted.';
@override @override
String get freeFeature1 => '3 Media file uploads per day'; String get freeFeature1 => '10 Media file uploads per day';
@override @override
String get plusFeature1 => '✓ Unlimited media file uploads'; String get plusFeature1 => '✓ Unlimited media file uploads';
@override
String get plusFeature2 => 'Additional features (coming-soon)';
@override @override
String get transactionHistory => 'Your transaction history'; String get transactionHistory => 'Your transaction history';
@ -1063,12 +1066,5 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get testPaymentMethod => 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!'; 'Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!';
@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.';
} }

View file

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

View file

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

View file

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

View file

@ -88,10 +88,12 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isConnected = context.watch<CustomChangeProvider>().isConnected; final isConnected = context.watch<CustomChangeProvider>().isConnected;
final planId = context.watch<CustomChangeProvider>().plan;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row(children: [ title: Row(children: [
const Text('twonly '), const Text('twonly '),
if (planId != 'Free')
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) { Navigator.push(context, MaterialPageRoute(builder: (context) {
@ -105,7 +107,7 @@ class _ChatListViewState extends State<ChatListView> {
), ),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
child: Text( child: Text(
context.watch<CustomChangeProvider>().plan, planId,
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View file

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

View file

@ -2,7 +2,6 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.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/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.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/components/better_list_title.dart';
import 'package:twonly/src/views/settings/subscription/additional_users.view.dart'; import 'package:twonly/src/views/settings/subscription/additional_users.view.dart';
import 'package:twonly/src/views/settings/subscription/checkout.view.dart'; import 'package:twonly/src/views/settings/subscription/checkout.view.dart';
@ -129,59 +127,21 @@ class _SubscriptionViewState extends State<SubscriptionView> {
additionalOwnerName = ownerId.toString(); additionalOwnerName = ownerId.toString();
} }
} }
final user = await getUser();
if (user != null) {
testerRequested = user.requestedTesterAccount;
// if (kDebugMode) {
// testerRequested = false;
// }
}
setState(() {}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final myLocale = Localizations.localeOf(context); final myLocale = Localizations.localeOf(context);
String? formattedBalance; String? formattedBalance;
DateTime? nextPayment; DateTime? nextPayment;
final currentPlan = context.read<CustomChangeProvider>().plan; final currentPlan = context.read<CustomChangeProvider>().plan;
final isAdditionalUser = currentPlan == 'Free' || currentPlan == 'Plus'; final isPayingUser = currentPlan == 'Family' || currentPlan == 'Pro';
if (ballance != null) { if (ballance != null) {
final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch( final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch(
ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000); ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000);
if (!isAdditionalUser) { if (isPayingUser) {
nextPayment = lastPaymentDateTime nextPayment = lastPaymentDateTime
.add(Duration(days: ballance!.paymentPeriodDays.toInt())); .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') if (currentPlan != 'Family' && currentPlan != 'Pro')
PlanCard( PlanCard(
planId: 'Pro', planId: 'Pro',
@ -307,7 +253,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
await initAsync(); await initAsync();
}, },
), ),
if (currentPlan == 'Preview' || currentPlan == 'Free') ...[ if (!isPayingUser) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
Center( Center(
child: Padding( child: Padding(
@ -320,14 +266,6 @@ class _SubscriptionViewState extends State<SubscriptionView> {
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
if (currentPlan != 'Free')
PlanCard(
planId: 'Free',
onTap: () async {
await redeemUserInviteCode(context, 'Free');
await initAsync();
},
),
PlanCard( PlanCard(
planId: 'Plus', planId: 'Plus',
onTap: () async { onTap: () async {
@ -338,7 +276,6 @@ class _SubscriptionViewState extends State<SubscriptionView> {
], ],
const SizedBox(height: 10), const SizedBox(height: 10),
if (currentPlan != 'Family') const Divider(), if (currentPlan != 'Family') const Divider(),
if (currentPlan != 'Preview')
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.gears, icon: FontAwesomeIcons.gears,
text: context.lang.manageSubscription, text: context.lang.manageSubscription,
@ -373,7 +310,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
})); }));
}, },
), ),
if (!isAdditionalUser) if (isPayingUser)
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.userPlus, icon: FontAwesomeIcons.userPlus,
text: context.lang.manageAdditionalUsers, text: context.lang.manageAdditionalUsers,
@ -434,24 +371,25 @@ class PlanCard extends StatelessWidget {
final yearlyPrice = getPlanPrice(planId, paidMonthly: false); final yearlyPrice = getPlanPrice(planId, paidMonthly: false);
final monthlyPrice = getPlanPrice(planId, paidMonthly: true); final monthlyPrice = getPlanPrice(planId, paidMonthly: true);
var features = <String>[]; var features = <String>[];
final isPayingUser = planId == 'Family' || planId == 'Pro';
switch (planId) { switch (planId) {
case 'Free': case 'Free':
features = [context.lang.freeFeature1]; features = [context.lang.freeFeature1];
case 'Plus': case 'Plus':
features = [context.lang.plusFeature1]; features = [context.lang.plusFeature1, context.lang.plusFeature2];
case 'Tester': case 'Tester':
case 'Pro': case 'Pro':
features = [ features = [
context.lang.proFeature1, context.lang.proFeature1,
context.lang.proFeature2, context.lang.proFeature2,
context.lang.proFeature3 context.lang.proFeature3,
context.lang.proFeature4,
]; ];
case 'Family': case 'Family':
features = [ features = [
context.lang.familyFeature1, context.lang.familyFeature1,
context.lang.familyFeature2, context.lang.familyFeature2,
context.lang.familyFeature3
]; ];
default: default:
} }
@ -481,7 +419,7 @@ class PlanCard extends StatelessWidget {
), ),
), ),
if (yearlyPrice != 0) const SizedBox(height: 10), if (yearlyPrice != 0) const SizedBox(height: 10),
if (yearlyPrice != 0 && paidMonthly == null) if (isPayingUser)
Column( Column(
children: [ children: [
if (paidMonthly == null || paidMonthly!) if (paidMonthly == null || paidMonthly!)
@ -504,7 +442,7 @@ class PlanCard extends StatelessWidget {
), ),
], ],
), ),
if (paidMonthly != null) if (isPayingUser && paidMonthly != null)
Text( Text(
(paidMonthly!) (paidMonthly!)
? '${localePrizing(context, monthlyPrice)}/${context.lang.month}' ? '${localePrizing(context, monthlyPrice)}/${context.lang.month}'