From db96157ff52a374d983ed386e83a2c3f523996a4 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 11 May 2025 11:53:25 +0200 Subject: [PATCH] error msg when limit is reached --- lib/src/localization/app_de.arb | 4 +- lib/src/localization/app_en.arb | 4 +- .../generated/app_localizations.dart | 12 ++++ .../generated/app_localizations_de.dart | 6 ++ .../generated/app_localizations_en.dart | 6 ++ lib/src/model/json/userdata.dart | 3 + lib/src/model/json/userdata.g.dart | 8 ++- lib/src/providers/api/media_send.dart | 51 ++++++++++++++--- lib/src/utils/misc.dart | 7 +++ .../views/camera/share_image_editor_view.dart | 40 +++++++++---- lib/src/views/camera/share_image_view.dart | 56 ++++++++++++------- .../subscription/subscription_view.dart | 23 +++++++- 12 files changed, 177 insertions(+), 43 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index ab2c91e..cd810ad 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -223,5 +223,7 @@ "checkoutSubmit": "Kostenpflichtig bestellen", "additionalUsersList": "Ihre zusätzlichen 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." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index b6c5cd0..93b55a8 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -382,5 +382,7 @@ "checkoutSubmit": "Order with a fee.", "additionalUsersList": "Your additional users", "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." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 720d7ac..eca2c4d 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1354,6 +1354,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'twonly-Codes für \"Free\" user'** 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 { diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 61be404..d87127d 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -651,4 +651,10 @@ class AppLocalizationsDe extends AppLocalizations { @override 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.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 906e44e..be1e1a8 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -651,4 +651,10 @@ class AppLocalizationsEn extends AppLocalizations { @override 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.'; } diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 06c9f6e..c886817 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -31,6 +31,9 @@ class UserData { String? lastPlanBallance; String? additionalUserInvites; + DateTime? lastImageSend; + int? todaysImageCounter; + final int userId; factory UserData.fromJson(Map json) => diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 781bb3f..ab81bdb 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -31,7 +31,11 @@ UserData _$UserDataFromJson(Map json) => UserData( ?.map((e) => e as String) .toList() ..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 _$UserDataToJson(UserData instance) => { 'username': instance.username, @@ -49,6 +53,8 @@ Map _$UserDataToJson(UserData instance) => { 'lastUsedEditorEmojis': instance.lastUsedEditorEmojis, 'lastPlanBallance': instance.lastPlanBallance, 'additionalUserInvites': instance.additionalUserInvites, + 'lastImageSend': instance.lastImageSend?.toIso8601String(), + 'todaysImageCounter': instance.todaysImageCounter, 'userId': instance.userId, }; diff --git a/lib/src/providers/api/media_send.dart b/lib/src/providers/api/media_send.dart index 867387b..d520df0 100644 --- a/lib/src/providers/api/media_send.dart +++ b/lib/src/providers/api/media_send.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:logging/logging.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/media_received.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'; +Future 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( - List userIds, - Uint8List imageBytes, - bool isRealTwonly, - int maxShowTime, - XFile? videoFilePath, - bool? enableVideoAudio, - bool mirrorVideo) async { + List userIds, + Uint8List imageBytes, + bool isRealTwonly, + int maxShowTime, + XFile? videoFilePath, + bool? enableVideoAudio, + bool mirrorVideo, +) async { MediaUploadMetadata metadata = MediaUploadMetadata(); metadata.contactIds = userIds; metadata.isRealTwonly = isRealTwonly; @@ -314,6 +343,14 @@ Future handleGetUploadToken(MediaUpload media) async { await apiProvider.getUploadToken(media.metadata.contactIds.length); 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") .shout("Will be tried again when reconnected to server!"); return false; diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 0efb2e9..1184d1b 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -194,3 +194,10 @@ bool isDarkMode(BuildContext context) { return selectedTheme == ThemeMode.dark || (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; +} diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index e69bb47..15e40de 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:logging/logging.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/views/camera/components/save_to_gallery.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/modules/all_emojis.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:twonly/src/views/settings/subscription/subscription_view.dart'; import 'package:video_player/video_player.dart'; List layers = []; @@ -365,18 +367,32 @@ class _ShareImageEditorView extends State { Navigator.pop(context, true); return; } - sendMediaFile( - [widget.sendTo!.userId], - imageBytes, - _isRealTwonly, - maxShowTime, - widget.videoFilePath, - videoWithAudio, - widget.mirrorVideo, - ); - if (context.mounted) { - // ignore: use_build_context_synchronously - Navigator.pop(context, true); + ErrorCode? err = await isAllowedToSend(); + if (!context.mounted) return; + + if (err != null) { + setState(() { + sendingOrLoadingImage = false; + }); + await Navigator.push(context, MaterialPageRoute(builder: (context) { + return SubscriptionView( + redirectError: err, + ); + })); + } 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); + } } } diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 06faed5..58ec1d0 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -5,6 +5,7 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.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/views/camera/components/best_friends_selector.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/twonly_database.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/settings/subscription/subscription_view.dart'; class ShareImageView extends StatefulWidget { const ShareImageView( @@ -243,26 +245,40 @@ class _ShareImageView extends State { if (imageBytes == null || _selectedUserIds.isEmpty) { return; } - setState(() { - sendingImage = true; - }); - sendMediaFile( - _selectedUserIds.toList(), - 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); - // } + + ErrorCode? err = await isAllowedToSend(); + if (!context.mounted) return; + + if (err != null) { + await Navigator.push(context, + MaterialPageRoute(builder: (context) { + return SubscriptionView( + redirectError: err, + ); + })); + } else { + setState(() { + sendingImage = true; + }); + + sendMediaFile( + _selectedUserIds.toList(), + 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( diff --git a/lib/src/views/settings/subscription/subscription_view.dart b/lib/src/views/settings/subscription/subscription_view.dart index f65487a..355bb3e 100644 --- a/lib/src/views/settings/subscription/subscription_view.dart +++ b/lib/src/views/settings/subscription/subscription_view.dart @@ -5,6 +5,7 @@ import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.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/providers/connection_provider.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -82,7 +83,9 @@ int calculateRefund(Response_PlanBallance current) { } class SubscriptionView extends StatefulWidget { - const SubscriptionView({super.key}); + const SubscriptionView({super.key, this.redirectError}); + + final ErrorCode? redirectError; @override State createState() => _SubscriptionViewState(); @@ -135,6 +138,24 @@ class _SubscriptionViewState extends State { ), body: ListView( 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: const EdgeInsets.all(32.0), child: Center(