error msg when limit is reached

This commit is contained in:
otsmr 2025-05-11 11:53:25 +02:00
parent c5734bbcc6
commit db96157ff5
12 changed files with 177 additions and 43 deletions

View file

@ -223,5 +223,7 @@
"checkoutSubmit": "Kostenpflichtig bestellen", "checkoutSubmit": "Kostenpflichtig bestellen",
"additionalUsersList": "Ihre zusätzlichen Benutzer", "additionalUsersList": "Ihre zusätzlichen Benutzer",
"additionalUsersPlusTokens": "twonly-Codes für \"Plus\"-Benutzer", "additionalUsersPlusTokens": "twonly-Codes für \"Plus\"-Benutzer",
"additionalUsersFreeTokens": "twonly-Codes für \"Free\"-Benutzer" "additionalUsersFreeTokens": "twonly-Codes für \"Free\"-Benutzer",
"planNotAllowed": "In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.",
"planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden."
} }

View file

@ -382,5 +382,7 @@
"checkoutSubmit": "Order with a fee.", "checkoutSubmit": "Order with a fee.",
"additionalUsersList": "Your additional users", "additionalUsersList": "Your additional users",
"additionalUsersPlusTokens": "twonly-Codes für \"Plus\" user", "additionalUsersPlusTokens": "twonly-Codes für \"Plus\" user",
"additionalUsersFreeTokens": "twonly-Codes für \"Free\" user" "additionalUsersFreeTokens": "twonly-Codes für \"Free\" user",
"planLimitReached": "You have reached your plan limit for today. Upgrade your plan now to send the media file.",
"planNotAllowed": "You cannot send media files with your current tariff. Upgrade your plan now to send the media file."
} }

View file

@ -1354,6 +1354,18 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'twonly-Codes für \"Free\" user'** /// **'twonly-Codes für \"Free\" user'**
String get additionalUsersFreeTokens; String get additionalUsersFreeTokens;
/// No description provided for @planLimitReached.
///
/// In en, this message translates to:
/// **'You have reached your plan limit for today. Upgrade your plan now to send the media file.'**
String get planLimitReached;
/// No description provided for @planNotAllowed.
///
/// In en, this message translates to:
/// **'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.'**
String get planNotAllowed;
} }
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> { class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {

View file

@ -651,4 +651,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get additionalUsersFreeTokens => 'twonly-Codes für \"Free\"-Benutzer'; String get additionalUsersFreeTokens => 'twonly-Codes für \"Free\"-Benutzer';
@override
String get planLimitReached => 'Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.';
@override
String get planNotAllowed => 'In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.';
} }

View file

@ -651,4 +651,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get additionalUsersFreeTokens => 'twonly-Codes für \"Free\" user'; String get additionalUsersFreeTokens => 'twonly-Codes für \"Free\" user';
@override
String get planLimitReached => 'You have reached your plan limit for today. Upgrade your plan now to send the media file.';
@override
String get planNotAllowed => 'You cannot send media files with your current tariff. Upgrade your plan now to send the media file.';
} }

View file

@ -31,6 +31,9 @@ class UserData {
String? lastPlanBallance; String? lastPlanBallance;
String? additionalUserInvites; String? additionalUserInvites;
DateTime? lastImageSend;
int? todaysImageCounter;
final int userId; final int userId;
factory UserData.fromJson(Map<String, dynamic> json) => factory UserData.fromJson(Map<String, dynamic> json) =>

View file

@ -31,7 +31,11 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
?.map((e) => e as String) ?.map((e) => e as String)
.toList() .toList()
..lastPlanBallance = json['lastPlanBallance'] as String? ..lastPlanBallance = json['lastPlanBallance'] as String?
..additionalUserInvites = json['additionalUserInvites'] as String?; ..additionalUserInvites = json['additionalUserInvites'] as String?
..lastImageSend = json['lastImageSend'] == null
? null
: DateTime.parse(json['lastImageSend'] as String)
..todaysImageCounter = (json['todaysImageCounter'] as num?)?.toInt();
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'username': instance.username, 'username': instance.username,
@ -49,6 +53,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'lastUsedEditorEmojis': instance.lastUsedEditorEmojis, 'lastUsedEditorEmojis': instance.lastUsedEditorEmojis,
'lastPlanBallance': instance.lastPlanBallance, 'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites, 'additionalUserInvites': instance.additionalUserInvites,
'lastImageSend': instance.lastImageSend?.toIso8601String(),
'todaysImageCounter': instance.todaysImageCounter,
'userId': instance.userId, 'userId': instance.userId,
}; };

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
@ -19,16 +20,44 @@ import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:twonly/src/providers/api/media_received.dart'; import 'package:twonly/src/providers/api/media_received.dart';
import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/services/notification_service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:video_compress/video_compress.dart'; import 'package:video_compress/video_compress.dart';
Future<ErrorCode?> isAllowedToSend() async {
final user = await getUser();
if (user == null) return null;
if (user.subscriptionPlan == "Preview") {
return ErrorCode.PlanNotAllowed;
}
if (user.subscriptionPlan == "Free") {
if (user.lastImageSend != null && user.todaysImageCounter != null) {
if (isToday(user.lastImageSend!)) {
if (user.todaysImageCounter == 3) {
return ErrorCode.PlanLimitReached;
}
user.todaysImageCounter = user.todaysImageCounter! + 1;
} else {
user.todaysImageCounter = 1;
}
} else {
user.todaysImageCounter = 1;
}
user.lastImageSend = DateTime.now();
await updateUser(user);
}
return null;
}
Future sendMediaFile( Future sendMediaFile(
List<int> userIds, List<int> userIds,
Uint8List imageBytes, Uint8List imageBytes,
bool isRealTwonly, bool isRealTwonly,
int maxShowTime, int maxShowTime,
XFile? videoFilePath, XFile? videoFilePath,
bool? enableVideoAudio, bool? enableVideoAudio,
bool mirrorVideo) async { bool mirrorVideo,
) async {
MediaUploadMetadata metadata = MediaUploadMetadata(); MediaUploadMetadata metadata = MediaUploadMetadata();
metadata.contactIds = userIds; metadata.contactIds = userIds;
metadata.isRealTwonly = isRealTwonly; metadata.isRealTwonly = isRealTwonly;
@ -314,6 +343,14 @@ Future<bool> handleGetUploadToken(MediaUpload media) async {
await apiProvider.getUploadToken(media.metadata.contactIds.length); await apiProvider.getUploadToken(media.metadata.contactIds.length);
if (res.isError || !res.value.hasUploadtoken()) { if (res.isError || !res.value.hasUploadtoken()) {
if (res.isError) {
if (res.error == ErrorCode.PlanNotAllowed) {
throw Exception("PlanNotAllowed");
}
if (res.error == ErrorCode.PlanLimitReached) {
throw Exception("PlanLimitReached");
}
}
Logger("media_send.dart") Logger("media_send.dart")
.shout("Will be tried again when reconnected to server!"); .shout("Will be tried again when reconnected to server!");
return false; return false;

View file

@ -194,3 +194,10 @@ bool isDarkMode(BuildContext context) {
return selectedTheme == ThemeMode.dark || return selectedTheme == ThemeMode.dark ||
(selectedTheme == ThemeMode.system && isDarkMode); (selectedTheme == ThemeMode.system && isDarkMode);
} }
bool isToday(DateTime lastImageSend) {
final now = DateTime.now();
return lastImageSend.year == now.year &&
lastImageSend.month == now.month &&
lastImageSend.day == now.day;
}

View file

@ -5,6 +5,7 @@ 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:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/error.pb.dart' show ErrorCode;
import 'package:twonly/src/providers/api/media_send.dart'; import 'package:twonly/src/providers/api/media_send.dart';
import 'package:twonly/src/views/camera/components/save_to_gallery.dart'; import 'package:twonly/src/views/camera/components/save_to_gallery.dart';
import 'package:twonly/src/views/camera/image_editor/action_button.dart'; import 'package:twonly/src/views/camera/image_editor/action_button.dart';
@ -21,6 +22,7 @@ import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
import 'package:twonly/src/views/camera/image_editor/layers_viewer.dart'; import 'package:twonly/src/views/camera/image_editor/layers_viewer.dart';
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/views/settings/subscription/subscription_view.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
List<Layer> layers = []; List<Layer> layers = [];
@ -365,18 +367,32 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Navigator.pop(context, true); Navigator.pop(context, true);
return; return;
} }
sendMediaFile( ErrorCode? err = await isAllowedToSend();
[widget.sendTo!.userId], if (!context.mounted) return;
imageBytes,
_isRealTwonly, if (err != null) {
maxShowTime, setState(() {
widget.videoFilePath, sendingOrLoadingImage = false;
videoWithAudio, });
widget.mirrorVideo, await Navigator.push(context, MaterialPageRoute(builder: (context) {
); return SubscriptionView(
if (context.mounted) { redirectError: err,
// ignore: use_build_context_synchronously );
Navigator.pop(context, true); }));
} else {
sendMediaFile(
[widget.sendTo!.userId],
imageBytes,
_isRealTwonly,
maxShowTime,
widget.videoFilePath,
videoWithAudio,
widget.mirrorVideo,
);
if (context.mounted) {
// ignore: use_build_context_synchronously
Navigator.pop(context, true);
}
} }
} }

View file

@ -5,6 +5,7 @@ import 'package:camera/camera.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:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/error.pb.dart';
import 'package:twonly/src/providers/api/media_send.dart'; import 'package:twonly/src/providers/api/media_send.dart';
import 'package:twonly/src/views/camera/components/best_friends_selector.dart'; import 'package:twonly/src/views/camera/components/best_friends_selector.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
@ -14,6 +15,7 @@ import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/settings/subscription/subscription_view.dart';
class ShareImageView extends StatefulWidget { class ShareImageView extends StatefulWidget {
const ShareImageView( const ShareImageView(
@ -243,26 +245,40 @@ class _ShareImageView extends State<ShareImageView> {
if (imageBytes == null || _selectedUserIds.isEmpty) { if (imageBytes == null || _selectedUserIds.isEmpty) {
return; return;
} }
setState(() {
sendingImage = true; ErrorCode? err = await isAllowedToSend();
}); if (!context.mounted) return;
sendMediaFile(
_selectedUserIds.toList(), if (err != null) {
imageBytes!, await Navigator.push(context,
widget.isRealTwonly, MaterialPageRoute(builder: (context) {
widget.maxShowTime, return SubscriptionView(
widget.videoFilePath, redirectError: err,
widget.enableVideoAudio, );
widget.mirrorVideo, }));
); } else {
if (context.mounted) { setState(() {
Navigator.pop(context, true); sendingImage = true;
// if (widget.preselectedUser != null) { });
// Navigator.pop(context, true);
// } else { sendMediaFile(
// Navigator.popUntil(context, (route) => route.isFirst, true); _selectedUserIds.toList(),
// globalUpdateOfHomeViewPageIndex(1); imageBytes!,
// } widget.isRealTwonly,
widget.maxShowTime,
widget.videoFilePath,
widget.enableVideoAudio,
widget.mirrorVideo,
);
if (context.mounted) {
Navigator.pop(context, true);
// if (widget.preselectedUser != null) {
// Navigator.pop(context, true);
// } else {
// Navigator.popUntil(context, (route) => route.isFirst, true);
// globalUpdateOfHomeViewPageIndex(1);
// }
}
} }
}, },
style: ButtonStyle( style: ButtonStyle(

View file

@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/server_to_client.pb.dart'; import 'package:twonly/src/model/protobuf/api/server_to_client.pb.dart';
import 'package:twonly/src/providers/connection_provider.dart'; import 'package:twonly/src/providers/connection_provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -82,7 +83,9 @@ int calculateRefund(Response_PlanBallance current) {
} }
class SubscriptionView extends StatefulWidget { class SubscriptionView extends StatefulWidget {
const SubscriptionView({super.key}); const SubscriptionView({super.key, this.redirectError});
final ErrorCode? redirectError;
@override @override
State<SubscriptionView> createState() => _SubscriptionViewState(); State<SubscriptionView> createState() => _SubscriptionViewState();
@ -135,6 +138,24 @@ class _SubscriptionViewState extends State<SubscriptionView> {
), ),
body: ListView( body: ListView(
children: [ children: [
if (widget.redirectError != null)
Center(
child: Container(
padding: const EdgeInsets.all(16),
margin: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orangeAccent,
borderRadius: BorderRadius.circular(15),
),
child: Text(
(widget.redirectError == ErrorCode.PlanLimitReached)
? context.lang.planLimitReached
: context.lang.planNotAllowed,
style: TextStyle(color: Colors.black),
textAlign: TextAlign.center,
),
),
),
Padding( Padding(
padding: const EdgeInsets.all(32.0), padding: const EdgeInsets.all(32.0),
child: Center( child: Center(