vouchers work

This commit is contained in:
otsmr 2025-05-08 21:37:14 +02:00
parent b5a8e785ee
commit f83cc4ace6
13 changed files with 1020 additions and 162 deletions

View file

@ -41,20 +41,25 @@ class _AppState extends State<App> with WidgetsBindingObserver {
// register global callbacks to the widget tree
globalCallbackConnectionState = (update) {
context.read<CustomChangeProvider>().updateConnectionState(update);
setUserPlan();
setupNotificationWithUsers();
};
initAsync();
}
Future initAsync() async {
apiProvider.connect();
Future setUserPlan() async {
final user = await getUser();
if (user != null && context.mounted) {
context.read<CustomChangeProvider>().updatePlan(user.subscriptionPlan);
}
}
Future initAsync() async {
setUserPlan();
apiProvider.connect();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);

View file

@ -15,12 +15,14 @@
"onboardingBuyOneGetTwoTitle": "Kaufe eins, bekomme zwei",
"onboardingBuyOneGetTwoBody": "twonly benötigt immer mindestens zwei Personen, daher erhältst du beim Kauf eine zweite kostenlose Lizenz für deinen twonly-Partner.",
"onboardingGetStartedTitle": "Auf geht's",
"onboardingGetStartedBody": "Du kannst twonly 14 Tage lang kostenlos testen, danach kostet es entweder 1€/Monat oder 9€/Jahr.",
"onboardingGetStartedBody": "Du kannst twonly kostenlos im Preview-Modus testen. In diesem Modus kannst du von anderen gefunden werden und Bilder oder Videos empfangen, aber du kannst selbst keine senden.",
"onboardingTryForFree": "Kostenlos testen",
"registerUsernameSlogan": "Bitte wähle einen Benutzernamen, damit dich andere finden können!",
"registerUsernameDecoration": "Benutzername",
"registerUsernameLimits": "Der Benutzername muss 3 bis 12 Zeichen lang sein und darf nur aus Buchstaben (a-z) und Zahlen (0-9) bestehen.",
"registerSubmitButton": "Jetzt registrieren!",
"registerTwonlyCodeText": "Hast du einen twonly-Code erhalten? Dann löse ihn entweder direkt hier oder später ein!",
"registerTwonlyCodeLabel": "twonly-Code",
"newMessageTitle": "Neue Nachricht",
"chatsTapToSend": "Klicke, um dein erstes Bild zu teilen.",
"cameraPreviewSendTo": "Senden an",
@ -161,5 +163,38 @@
"errorInvalidPublicKey": "Der von dir angegebene öffentliche Schlüssel ist ungültig. Bitte überprüfe den Schlüssel und versuche es erneut.",
"errorSessionAlreadyAuthenticated": "Du bist bereits angemeldet. Bitte melde dich ab, wenn du dich mit einem anderen Konto anmelden möchtest.",
"errorSessionNotAuthenticated": "Deine Sitzung ist nicht authentifiziert. Bitte melde dich an, um fortzufahren.",
"errorOnlyOneSessionAllowed": "Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren."
"errorOnlyOneSessionAllowed": "Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren.",
"upgradeToPaidPlan": "Upgrade auf einen kostenpflichtigen Plan.",
"errorNotEnoughCredit": "Du hast nicht genügend twonly-Guthaben.",
"errorPlanLimitReached": "Du hast das Limit deines Plans erreicht. Bitte upgrade deinen Plan.",
"errorPlanNotAllowed": "Dieses Feature ist in deinem aktuellen Plan nicht verfügbar.",
"errorVoucherInvalid": "Der eingegebene Gutschein-Code ist nicht gültig.",
"proYearlyPrice": "10€/Jahr",
"proMonthlyPrice": "1€/Monat",
"proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"proFeature2": "1 zusätzlicher Plus Benutzer",
"proFeature3": "3 zusätzliche kostenlose Benutzer",
"familyYearlyPrice": "20€/Jahr",
"familyMonthlyPrice": "2€/Monat",
"familyFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"familyFeature2": "4 zusätzliche Plus Benutzer",
"familyFeature3": "5 zusätzliche kostenlose Benutzer",
"redeemUserInviteCode": "Oder löse einen zusätzlichen twonly-Code ein.",
"freeFeature1": "3 Medien-Datei-Uploads pro Tag",
"plusFeature1": "✓ Unbegrenzte Medien-Datei-Uploads",
"transactionHistory": "Transaktionshistorie",
"currentBalance": "Aktueller Kontostand",
"manageAdditionalUsers": "Zusätzlichen Benutzer verwalten",
"open": "Offene",
"buy": "Kaufen",
"createOrRedeemVoucher": "Gutschein erstellen oder einlösen",
"createVoucher": "Gutschein kaufen",
"createVoucherDesc": "Wähle den Wert des Gutscheins. Der Wert des Gutschein wird von deinem twonly-Guthaben abgezogen.",
"redeemVoucher": "Gutschein einlösen",
"voucherCreated": "Gutschein wurde erstellt",
"openVouchers": "Offene Gutscheine",
"enterVoucherCode": "Gutschein Code eingeben",
"voucherRedeemed": "Gutschein eingelöst",
"requestedVouchers": "Beantragte Gutscheine",
"redeemedVouchers": "Eingelöste Gutscheine"
}

View file

@ -30,7 +30,7 @@
"@onboardingBuyOneGetTwoBody": {},
"onboardingGetStartedTitle": "Let's go!",
"@onboardingGetStartedTitle": {},
"onboardingGetStartedBody": "You can test twonly free of charge for 14 days, after that it costs either 1€/month or 9€/year.",
"onboardingGetStartedBody": "You can test twonly free of charge in preview mode. In this mode you can be found by others and receive pictures or videos but you cannot send any yourself.",
"@onboardingGetStartedBody": {},
"onboardingTryForFree": "Try for free",
"@onboardingTryForFree": {},
@ -42,6 +42,8 @@
"@registerUsernameLimits": {},
"registerSubmitButton": "Register now!",
"@registerSubmitButton": {},
"registerTwonlyCodeText": "Have you received a twonly code? Then redeem it either directly here or later!",
"registerTwonlyCodeLabel": "twonly-Code",
"newMessageTitle": "New message",
"@newMessageTitle": {},
"chatsTapToSend": "Click to send your first image",
@ -319,5 +321,38 @@
"errorSessionNotAuthenticated": "Your session is not authenticated. Please log in to continue.",
"@errorSessionNotAuthenticated": {},
"errorOnlyOneSessionAllowed": "Only one active session is allowed per user. Please log out from other devices to continue.",
"@errorOnlyOneSessionAllowed": {}
"@errorOnlyOneSessionAllowed": {},
"errorNotEnoughCredit": "You do not have enough twonly-credit.",
"errorVoucherInvalid": "The voucher code you entered is not valid.",
"errorPlanLimitReached": "You have reached your plans limit. Please upgrade your plan.",
"errorPlanNotAllowed": "This feature is not available in your current plan.",
"upgradeToPaidPlan": "Upgrade to a paid plan.",
"proYearlyPrice": "10€/year",
"proMonthlyPrice": "1€/month",
"proFeature1": "✓ Unlimited media file uploads",
"proFeature2": "1 additional Plus user",
"proFeature3": "3 additional Free users",
"familyYearlyPrice": "20€/year",
"familyMonthlyPrice": "2€/month",
"familyFeature1": "✓ Unlimited media file uploads",
"familyFeature2": "4 additional Plus users",
"familyFeature3": "5 additional Free users",
"redeemUserInviteCode": "Or redeem an additional user invite code.",
"freeFeature1": "3 Media file uploads per day",
"plusFeature1": "✓ Unlimited media file uploads",
"transactionHistory": "Your transaction history",
"currentBalance": "Current balance",
"manageAdditionalUsers": "Manage your additional users",
"open": "Open",
"createOrRedeemVoucher": "Buy or redeem voucher",
"createVoucher": "Buy voucher",
"createVoucherDesc": "Choose the value of the voucher. The value of the voucher will be deducted from your twonly balance.",
"redeemVoucher": "Redeem voucher",
"openVouchers": "Open vouchers",
"voucherCreated": "Voucher created",
"voucherRedeemed": "Voucher redeemed",
"enterVoucherCode": "Enter Voucher Code",
"requestedVouchers": "Requested vouchers",
"redeemedVouchers": "Redeemed vouchers",
"buy": "Buy"
}

View file

@ -188,7 +188,7 @@ abstract class AppLocalizations {
/// No description provided for @onboardingGetStartedBody.
///
/// In en, this message translates to:
/// **'You can test twonly free of charge for 14 days, after that it costs either 1€/month or 9€/year.'**
/// **'You can test twonly free of charge in preview mode. In this mode you can be found by others and receive pictures or videos but you cannot send any yourself.'**
String get onboardingGetStartedBody;
/// No description provided for @onboardingTryForFree.
@ -221,6 +221,18 @@ abstract class AppLocalizations {
/// **'Register now!'**
String get registerSubmitButton;
/// No description provided for @registerTwonlyCodeText.
///
/// In en, this message translates to:
/// **'Have you received a twonly code? Then redeem it either directly here or later!'**
String get registerTwonlyCodeText;
/// No description provided for @registerTwonlyCodeLabel.
///
/// In en, this message translates to:
/// **'twonly-Code'**
String get registerTwonlyCodeLabel;
/// No description provided for @newMessageTitle.
///
/// In en, this message translates to:
@ -982,6 +994,204 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Only one active session is allowed per user. Please log out from other devices to continue.'**
String get errorOnlyOneSessionAllowed;
/// No description provided for @errorNotEnoughCredit.
///
/// In en, this message translates to:
/// **'You do not have enough twonly-credit.'**
String get errorNotEnoughCredit;
/// No description provided for @errorVoucherInvalid.
///
/// In en, this message translates to:
/// **'The voucher code you entered is not valid.'**
String get errorVoucherInvalid;
/// No description provided for @errorPlanLimitReached.
///
/// In en, this message translates to:
/// **'You have reached your plans limit. Please upgrade your plan.'**
String get errorPlanLimitReached;
/// No description provided for @errorPlanNotAllowed.
///
/// In en, this message translates to:
/// **'This feature is not available in your current plan.'**
String get errorPlanNotAllowed;
/// No description provided for @upgradeToPaidPlan.
///
/// In en, this message translates to:
/// **'Upgrade to a paid plan.'**
String get upgradeToPaidPlan;
/// No description provided for @proYearlyPrice.
///
/// In en, this message translates to:
/// **'10€/year'**
String get proYearlyPrice;
/// No description provided for @proMonthlyPrice.
///
/// In en, this message translates to:
/// **'1€/month'**
String get proMonthlyPrice;
/// No description provided for @proFeature1.
///
/// In en, this message translates to:
/// **'✓ Unlimited media file uploads'**
String get proFeature1;
/// No description provided for @proFeature2.
///
/// In en, this message translates to:
/// **'1 additional Plus user'**
String get proFeature2;
/// No description provided for @proFeature3.
///
/// In en, this message translates to:
/// **'3 additional Free users'**
String get proFeature3;
/// No description provided for @familyYearlyPrice.
///
/// In en, this message translates to:
/// **'20€/year'**
String get familyYearlyPrice;
/// No description provided for @familyMonthlyPrice.
///
/// In en, this message translates to:
/// **'2€/month'**
String get familyMonthlyPrice;
/// No description provided for @familyFeature1.
///
/// In en, this message translates to:
/// **'✓ Unlimited media file uploads'**
String get familyFeature1;
/// No description provided for @familyFeature2.
///
/// In en, this message translates to:
/// **'4 additional Plus users'**
String get familyFeature2;
/// No description provided for @familyFeature3.
///
/// In en, this message translates to:
/// **'5 additional Free users'**
String get familyFeature3;
/// No description provided for @redeemUserInviteCode.
///
/// In en, this message translates to:
/// **'Or redeem an additional user invite code.'**
String get redeemUserInviteCode;
/// No description provided for @freeFeature1.
///
/// In en, this message translates to:
/// **'3 Media file uploads per day'**
String get freeFeature1;
/// No description provided for @plusFeature1.
///
/// In en, this message translates to:
/// **'✓ Unlimited media file uploads'**
String get plusFeature1;
/// No description provided for @transactionHistory.
///
/// In en, this message translates to:
/// **'Your transaction history'**
String get transactionHistory;
/// No description provided for @currentBalance.
///
/// In en, this message translates to:
/// **'Current balance'**
String get currentBalance;
/// No description provided for @manageAdditionalUsers.
///
/// In en, this message translates to:
/// **'Manage your additional users'**
String get manageAdditionalUsers;
/// No description provided for @open.
///
/// In en, this message translates to:
/// **'Open'**
String get open;
/// No description provided for @createOrRedeemVoucher.
///
/// In en, this message translates to:
/// **'Buy or redeem voucher'**
String get createOrRedeemVoucher;
/// No description provided for @createVoucher.
///
/// In en, this message translates to:
/// **'Buy voucher'**
String get createVoucher;
/// No description provided for @createVoucherDesc.
///
/// In en, this message translates to:
/// **'Choose the value of the voucher. The value of the voucher will be deducted from your twonly balance.'**
String get createVoucherDesc;
/// No description provided for @redeemVoucher.
///
/// In en, this message translates to:
/// **'Redeem voucher'**
String get redeemVoucher;
/// No description provided for @openVouchers.
///
/// In en, this message translates to:
/// **'Open vouchers'**
String get openVouchers;
/// No description provided for @voucherCreated.
///
/// In en, this message translates to:
/// **'Voucher created'**
String get voucherCreated;
/// No description provided for @voucherRedeemed.
///
/// In en, this message translates to:
/// **'Voucher redeemed'**
String get voucherRedeemed;
/// No description provided for @enterVoucherCode.
///
/// In en, this message translates to:
/// **'Enter Voucher Code'**
String get enterVoucherCode;
/// No description provided for @requestedVouchers.
///
/// In en, this message translates to:
/// **'Requested vouchers'**
String get requestedVouchers;
/// No description provided for @redeemedVouchers.
///
/// In en, this message translates to:
/// **'Redeemed vouchers'**
String get redeemedVouchers;
/// No description provided for @buy.
///
/// In en, this message translates to:
/// **'Buy'**
String get buy;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {

View file

@ -54,7 +54,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get onboardingGetStartedTitle => 'Auf geht\'s';
@override
String get onboardingGetStartedBody => 'Du kannst twonly 14 Tage lang kostenlos testen, danach kostet es entweder 1€/Monat oder 9€/Jahr.';
String get onboardingGetStartedBody => 'Du kannst twonly kostenlos im Preview-Modus testen. In diesem Modus kannst du von anderen gefunden werden und Bilder oder Videos empfangen, aber du kannst selbst keine senden.';
@override
String get onboardingTryForFree => 'Kostenlos testen';
@ -71,6 +71,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get registerSubmitButton => 'Jetzt registrieren!';
@override
String get registerTwonlyCodeText => 'Hast du einen twonly-Code erhalten? Dann löse ihn entweder direkt hier oder später ein!';
@override
String get registerTwonlyCodeLabel => 'twonly-Code';
@override
String get newMessageTitle => 'Neue Nachricht';
@ -463,4 +469,103 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get errorOnlyOneSessionAllowed => 'Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren.';
@override
String get errorNotEnoughCredit => 'Du hast nicht genügend twonly-Guthaben.';
@override
String get errorVoucherInvalid => 'Der eingegebene Gutschein-Code ist nicht gültig.';
@override
String get errorPlanLimitReached => 'Du hast das Limit deines Plans erreicht. Bitte upgrade deinen Plan.';
@override
String get errorPlanNotAllowed => 'Dieses Feature ist in deinem aktuellen Plan nicht verfügbar.';
@override
String get upgradeToPaidPlan => 'Upgrade auf einen kostenpflichtigen Plan.';
@override
String get proYearlyPrice => '10€/Jahr';
@override
String get proMonthlyPrice => '1€/Monat';
@override
String get proFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
@override
String get proFeature2 => '1 zusätzlicher Plus Benutzer';
@override
String get proFeature3 => '3 zusätzliche kostenlose Benutzer';
@override
String get familyYearlyPrice => '20€/Jahr';
@override
String get familyMonthlyPrice => '2€/Monat';
@override
String get familyFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
@override
String get familyFeature2 => '4 zusätzliche Plus Benutzer';
@override
String get familyFeature3 => '5 zusätzliche kostenlose Benutzer';
@override
String get redeemUserInviteCode => 'Oder löse einen zusätzlichen twonly-Code ein.';
@override
String get freeFeature1 => '3 Medien-Datei-Uploads pro Tag';
@override
String get plusFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
@override
String get transactionHistory => 'Transaktionshistorie';
@override
String get currentBalance => 'Aktueller Kontostand';
@override
String get manageAdditionalUsers => 'Zusätzlichen Benutzer verwalten';
@override
String get open => 'Offene';
@override
String get createOrRedeemVoucher => 'Gutschein erstellen oder einlösen';
@override
String get createVoucher => 'Gutschein kaufen';
@override
String get createVoucherDesc => 'Wähle den Wert des Gutscheins. Der Wert des Gutschein wird von deinem twonly-Guthaben abgezogen.';
@override
String get redeemVoucher => 'Gutschein einlösen';
@override
String get openVouchers => 'Offene Gutscheine';
@override
String get voucherCreated => 'Gutschein wurde erstellt';
@override
String get voucherRedeemed => 'Gutschein eingelöst';
@override
String get enterVoucherCode => 'Gutschein Code eingeben';
@override
String get requestedVouchers => 'Beantragte Gutscheine';
@override
String get redeemedVouchers => 'Eingelöste Gutscheine';
@override
String get buy => 'Kaufen';
}

View file

@ -54,7 +54,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingGetStartedTitle => 'Let\'s go!';
@override
String get onboardingGetStartedBody => 'You can test twonly free of charge for 14 days, after that it costs either 1€/month or 9€/year.';
String get onboardingGetStartedBody => 'You can test twonly free of charge in preview mode. In this mode you can be found by others and receive pictures or videos but you cannot send any yourself.';
@override
String get onboardingTryForFree => 'Try for free';
@ -71,6 +71,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get registerSubmitButton => 'Register now!';
@override
String get registerTwonlyCodeText => 'Have you received a twonly code? Then redeem it either directly here or later!';
@override
String get registerTwonlyCodeLabel => 'twonly-Code';
@override
String get newMessageTitle => 'New message';
@ -463,4 +469,103 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get errorOnlyOneSessionAllowed => 'Only one active session is allowed per user. Please log out from other devices to continue.';
@override
String get errorNotEnoughCredit => 'You do not have enough twonly-credit.';
@override
String get errorVoucherInvalid => 'The voucher code you entered is not valid.';
@override
String get errorPlanLimitReached => 'You have reached your plans limit. Please upgrade your plan.';
@override
String get errorPlanNotAllowed => 'This feature is not available in your current plan.';
@override
String get upgradeToPaidPlan => 'Upgrade to a paid plan.';
@override
String get proYearlyPrice => '10€/year';
@override
String get proMonthlyPrice => '1€/month';
@override
String get proFeature1 => '✓ Unlimited media file uploads';
@override
String get proFeature2 => '1 additional Plus user';
@override
String get proFeature3 => '3 additional Free users';
@override
String get familyYearlyPrice => '20€/year';
@override
String get familyMonthlyPrice => '2€/month';
@override
String get familyFeature1 => '✓ Unlimited media file uploads';
@override
String get familyFeature2 => '4 additional Plus users';
@override
String get familyFeature3 => '5 additional Free users';
@override
String get redeemUserInviteCode => 'Or redeem an additional user invite code.';
@override
String get freeFeature1 => '3 Media file uploads per day';
@override
String get plusFeature1 => '✓ Unlimited media file uploads';
@override
String get transactionHistory => 'Your transaction history';
@override
String get currentBalance => 'Current balance';
@override
String get manageAdditionalUsers => 'Manage your additional users';
@override
String get open => 'Open';
@override
String get createOrRedeemVoucher => 'Buy or redeem voucher';
@override
String get createVoucher => 'Buy voucher';
@override
String get createVoucherDesc => 'Choose the value of the voucher. The value of the voucher will be deducted from your twonly balance.';
@override
String get redeemVoucher => 'Redeem voucher';
@override
String get openVouchers => 'Open vouchers';
@override
String get voucherCreated => 'Voucher created';
@override
String get voucherRedeemed => 'Voucher redeemed';
@override
String get enterVoucherCode => 'Enter Voucher Code';
@override
String get requestedVouchers => 'Requested vouchers';
@override
String get redeemedVouchers => 'Redeemed vouchers';
@override
String get buy => 'Buy';
}

View file

@ -419,6 +419,34 @@ class ApiProvider {
return null;
}
Future<Response_Vouchers?> getVoucherList() async {
var get = ApplicationData_GetVouchers();
var appData = ApplicationData()..getvouchers = get;
var req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req);
if (res.isSuccess) {
server.Response_Ok ok = res.value;
if (ok.hasVouchers()) {
return ok.vouchers;
}
}
return null;
}
Future<Result> buyVoucher(int valueInCents) async {
var get = ApplicationData_CreateVoucher()..valueCents = valueInCents;
var appData = ApplicationData()..createvoucher = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
}
Future<Result> redeemVoucher(String voucher) async {
var get = ApplicationData_RedeemVoucher()..voucher = voucher;
var appData = ApplicationData()..redeemvoucher = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
}
Future<Result> updateFCMToken(String googleFcm) async {
var get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm;
var appData = ApplicationData()..updategooglefcmtoken = get;

View file

@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
bool _isConnected = false;
bool get isConnected => _isConnected;
String plan = "";
String plan = "Preview";
Future<void> updateConnectionState(bool update) async {
_isConnected = update;
notifyListeners();

View file

@ -68,33 +68,23 @@ Uint8List getRandomUint8List(int length) {
}
String errorCodeToText(BuildContext context, ErrorCode code) {
switch (code.toString()) {
case "Unknown":
return context.lang.errorUnknown;
case "BadRequest":
return context.lang.errorBadRequest;
case "TooManyRequests":
return context.lang.errorTooManyRequests;
case "InternalError":
switch (code) {
case ErrorCode.InternalError:
return context.lang.errorInternalError;
case "InvalidInvitationCode":
case ErrorCode.InvalidInvitationCode:
return context.lang.errorInvalidInvitationCode;
case "UsernameAlreadyTaken":
case ErrorCode.UsernameAlreadyTaken:
return context.lang.errorUsernameAlreadyTaken;
case "SignatureNotValid":
return context.lang.errorSignatureNotValid;
case "UsernameNotFound":
return context.lang.errorUsernameNotFound;
case "UsernameNotValid":
case ErrorCode.UsernameNotValid:
return context.lang.errorUsernameNotValid;
case "InvalidPublicKey":
return context.lang.errorInvalidPublicKey;
case "SessionAlreadyAuthenticated":
return context.lang.errorSessionAlreadyAuthenticated;
case "SessionNotAuthenticated":
return context.lang.errorSessionNotAuthenticated;
case "OnlyOneSessionAllowed":
return context.lang.errorOnlyOneSessionAllowed;
case ErrorCode.NotEnoughCredit:
return context.lang.errorNotEnoughCredit;
case ErrorCode.PlanLimitReached:
return context.lang.errorPlanLimitReached;
case ErrorCode.PlanNotAllowed:
return context.lang.errorPlanNotAllowed;
case ErrorCode.VoucherInValid:
return context.lang.errorVoucherInvalid;
default:
return code.toString(); // Fallback for unrecognized keys
}

View file

@ -3,22 +3,6 @@ import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'package:twonly/src/utils/misc.dart';
// Slide 1: Welcome to [App Name]
// Text: "Experience a new way to connect with friends through secure, spontaneous image sharing."
// Image Idea: A vibrant, welcoming graphic featuring diverse groups of friends using the app in various settings (e.g., at a café, at a party, etc.).
// Slide 2: End-to-End Encryption
// Text: "Your privacy matters. Enjoy peace of mind with end-to-end encryption, ensuring only you and your friends can see your images."
// Image Idea: A lock symbol overlaying a smartphone screen displaying an encrypted message, symbolizing security and privacy.
// Slide 3: Local Processing
// Text: "Everything is done locally. Our servers only see encrypted bytes, keeping your data safe from prying eyes."
// Image Idea: A visual representation of local processing, such as a smartphone with a shield icon, indicating that data remains on the device.
// Slide 4: Focus on Images
// Text: "Say goodbye to clutter! Our app is designed for sharing images, not useless distractions."
// Image Idea: A clean, minimalist interface showcasing a user effortlessly sending an image, with a focus on the image itself.
class OnboardingView extends StatelessWidget {
const OnboardingView({super.key, required this.callbackOnSuccess});
final Function callbackOnSuccess;

View file

@ -37,6 +37,10 @@ class _RegisterViewState extends State<RegisterView> {
final res = await apiProvider.register(username, inviteCode);
setState(() {
_isTryingToRegister = false;
});
if (res.isSuccess) {
Logger("create_new_user").info("Got user_id ${res.value} from server");
final userData = UserData(
@ -46,13 +50,6 @@ class _RegisterViewState extends State<RegisterView> {
subscriptionPlan: "Preview",
);
storage.write(key: "userData", value: jsonEncode(userData));
}
setState(() {
_isTryingToRegister = false;
});
if (res.isSuccess) {
apiProvider.authenticate();
widget.callbackOnSuccess();
return;
@ -130,31 +127,24 @@ class _RegisterViewState extends State<RegisterView> {
child: Text(
context.lang.registerUsernameLimits,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 7),
style: TextStyle(fontSize: 9),
),
),
),
// const SizedBox(height: 15),
// Center(
// child: Text(
// "To protect this small experimental project you need an invitation code! To get one just ask the right person!",
// textAlign: TextAlign.center,
// ),
// ),
// const SizedBox(height: 10),
// TextField(
// controller: inviteCodeController,
// decoration: getInputDecoration("Voucher code")),
// const SizedBox(height: 25),
// Center(
// child: Text(
// "Please ",
// textAlign: TextAlign.center,
// ),
// ),
const SizedBox(height: 50),
// Padding(
// padding: EdgeInsets.symmetric(horizontal: 10),
const SizedBox(height: 20),
Center(
child: Text(
context.lang.registerTwonlyCodeText,
textAlign: TextAlign.center,
),
),
const SizedBox(height: 10),
TextField(
controller: inviteCodeController,
decoration:
getInputDecoration(context.lang.registerTwonlyCodeLabel),
),
const SizedBox(height: 30),
Column(children: [
FilledButton.icon(
icon: _isTryingToRegister

View file

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
@ -7,6 +8,7 @@ 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';
import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/settings/subscription/voucher_view.dart';
class SubscriptionView extends StatefulWidget {
const SubscriptionView({super.key});
@ -16,8 +18,9 @@ class SubscriptionView extends StatefulWidget {
}
class _SubscriptionViewState extends State<SubscriptionView> {
bool hasInternet = true;
bool loaded = false;
int ballanceInCents = 0;
DateTime? nextPayment;
@override
void initState() {
@ -26,13 +29,16 @@ class _SubscriptionViewState extends State<SubscriptionView> {
}
Future initAsync() async {
// userData = await getUser();
// setState(() {});
Response_PlanBallance? ballance = await apiProvider.getPlanBallance();
if (ballance == null) {
if (ballance != null) {
setState(() {
hasInternet = false;
DateTime lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch(
ballance.lastPaymentDoneUnixTimestamp.toInt() * 1000);
nextPayment = lastPaymentDateTime
.add(Duration(days: ballance.paymentPeriodDays.toInt()));
ballanceInCents =
ballance.transactions.map((a) => a.depositCents.toInt()).sum;
loaded = true;
});
return;
}
@ -40,17 +46,20 @@ class _SubscriptionViewState extends State<SubscriptionView> {
@override
Widget build(BuildContext context) {
Locale myLocale = Localizations.localeOf(context);
String formattedBalance = NumberFormat.currency(
locale: 'de_DE', // Locale for Euro formatting
locale: myLocale.toString(),
symbol: '',
decimalDigits: 2,
).format(ballanceInCents / 100);
String currentPlan = context.read<CustomChangeProvider>().plan;
return Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsSubscription),
),
body: Column(
body: ListView(
children: [
Padding(
padding: const EdgeInsets.all(32.0),
@ -62,74 +71,120 @@ class _SubscriptionViewState extends State<SubscriptionView> {
),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 3),
child: Text(
context.watch<CustomChangeProvider>().plan,
currentPlan,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.black,
color: isDarkMode(context) ? Colors.black : Colors.white,
),
),
),
),
),
Expanded(
child: ListView(
children: [
if (currentPlan != "Family" && currentPlan != "Pro")
Center(
child: Text("Upgrade your current plan."),
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Text(
context.lang.upgradeToPaidPlan,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
SizedBox(height: 10),
),
),
if (currentPlan != "Family" && currentPlan != "Pro")
PlanCard(
title: 'Pro',
yearlyPrice: '10€/year',
monthlyPrice: '1€/month',
title: "Pro",
yearlyPrice: context.lang.proYearlyPrice,
monthlyPrice: context.lang.proMonthlyPrice,
features: [
'✓ Unlimited media files',
'1 additional Plus user',
'3 additional Free users',
context.lang.proFeature1,
context.lang.proFeature2,
context.lang.proFeature3,
],
),
SizedBox(height: 10),
if (currentPlan != "Family")
PlanCard(
title: 'Family',
yearlyPrice: '20€/year',
monthlyPrice: '2€/month',
title: "Family",
yearlyPrice: context.lang.familyYearlyPrice,
monthlyPrice: context.lang.familyMonthlyPrice,
features: [
'✓ All from Pro',
'4 additional Plus users',
'5 additional Free users',
context.lang.familyFeature1,
context.lang.familyFeature2,
context.lang.familyFeature3,
],
),
if (currentPlan == "Preview" || currentPlan == "Free") ...[
SizedBox(height: 10),
Divider(),
Center(
child: Padding(
padding: const EdgeInsets.all(14.0),
child: Text(
context.lang.redeemUserInviteCode,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
),
),
SizedBox(height: 10),
if (currentPlan != "Free")
PlanCard(
title: "Free",
yearlyPrice: "",
monthlyPrice: "",
features: [
context.lang.freeFeature1,
],
),
PlanCard(
title: "Plus",
yearlyPrice: "",
monthlyPrice: "",
features: [
context.lang.plusFeature1,
],
),
],
SizedBox(height: 10),
if (currentPlan != "Family") Divider(),
if (currentPlan == "Family" || currentPlan == "Pro")
BetterListTile(
icon: FontAwesomeIcons.ticket,
text: "Redeem code for additional user",
icon: FontAwesomeIcons.userPlus,
text: "Manage your subscription",
subtitle: (nextPayment != null)
? Text(
"Next payment: ${DateFormat.yMMMMd(myLocale.toString()).format(nextPayment!)}")
: null,
onTap: () {},
),
BetterListTile(
icon: FontAwesomeIcons.moneyBillTransfer,
text: "Your transaction history",
subtitle: Text("Current ballance: $formattedBalance"),
text: context.lang.transactionHistory,
subtitle: (loaded)
? Text("${context.lang.currentBalance}: $formattedBalance")
: null,
onTap: () {},
),
if (currentPlan == "Family" || currentPlan == "Pro")
BetterListTile(
icon: FontAwesomeIcons.userPlus,
text: "Manage your additional users",
subtitle: Text("Open: 3"),
text: context.lang.manageAdditionalUsers,
subtitle: (loaded) ? Text("${context.lang.open}: 3") : null,
onTap: () {},
),
BetterListTile(
icon: FontAwesomeIcons.gift,
text: "Create or redeem voucher",
onTap: () {},
icon: FontAwesomeIcons.ticket,
text: context.lang.createOrRedeemVoucher,
onTap: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) {
return VoucherView();
}));
initAsync();
},
),
SizedBox(height: 30)
],
// tranaction
),
)
],
),
);
}
@ -175,7 +230,8 @@ class PlanCard extends StatelessWidget {
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 10),
if (yearlyPrice != "") SizedBox(height: 10),
if (yearlyPrice != "")
Column(
children: [
Text(

View file

@ -0,0 +1,315 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/server_to_client.pb.dart';
import 'package:twonly/src/utils/misc.dart';
class VoucherView extends StatefulWidget {
const VoucherView({super.key});
@override
State<VoucherView> createState() => _VoucherViewState();
}
class _VoucherViewState extends State<VoucherView> {
List<Response_Voucher> vouchers = [];
@override
void initState() {
super.initState();
initAsync();
}
Future initAsync() async {
Response_Vouchers? resVouchers = await apiProvider.getVoucherList();
if (resVouchers != null) {
setState(() {
vouchers = resVouchers.vouchers;
});
return;
}
}
@override
Widget build(BuildContext context) {
final openVoucher = vouchers.where((x) => !x.redeemed && !x.requested);
final redeemedVoucher = vouchers.where((x) => x.redeemed);
// final requestedVoucher = vouchers.where((x) => !x.redeemed && x.requested);
return Scaffold(
appBar: AppBar(
title: Text(context.lang.createOrRedeemVoucher),
),
body: ListView(
children: [
ListTile(
title: Text(context.lang.redeemVoucher),
onTap: () async {
await redeemVoucher(context);
initAsync();
},
),
ListTile(
title: Text(context.lang.createVoucher),
onTap: () async {
await showBuyVoucher(context);
initAsync();
},
),
Divider(),
if (openVoucher.isNotEmpty)
ListTile(
title: Text(
context.lang.openVouchers,
style: TextStyle(fontSize: 13),
),
),
...openVoucher.map((x) => VoucherCard(voucher: x)),
// if (requestedVoucher.isNotEmpty)
// ListTile(
// title: Text(
// context.lang.requestedVouchers,
// style: TextStyle(fontSize: 13),
// ),
// ),
// ...requestedVoucher.map((x) => VoucherCard(voucher: x)),
if (redeemedVoucher.isNotEmpty)
ListTile(
title: Text(
context.lang.redeemedVouchers,
style: TextStyle(fontSize: 13),
),
),
...redeemedVoucher.map((x) => VoucherCard(voucher: x)),
],
),
);
}
}
class VoucherCard extends StatefulWidget {
final Response_Voucher voucher;
const VoucherCard({super.key, required this.voucher});
@override
State<VoucherCard> createState() => _VoucherCardState();
}
class _VoucherCardState extends State<VoucherCard> {
void _copyVoucherId() {
if (!widget.voucher.redeemed) {
Clipboard.setData(ClipboardData(text: widget.voucher.voucherId));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("${widget.voucher.voucherId} copied.")),
);
}
}
@override
Widget build(BuildContext context) {
bool isRedeemed = widget.voucher.redeemed || widget.voucher.requested;
Locale myLocale = Localizations.localeOf(context);
String formattedValue = NumberFormat.currency(
locale: myLocale.toString(),
symbol: '',
decimalDigits: 2,
).format(widget.voucher.valueCents.toInt() / 100);
return GestureDetector(
onTap: _copyVoucherId,
child: Card(
margin: const EdgeInsets.all(10),
elevation: 5,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.voucher.voucherId.toUpperCase(),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color:
(isRedeemed) ? Colors.grey : context.color.onSurface,
),
),
Text(
formattedValue,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color:
(isRedeemed) ? Colors.grey : context.color.onSurface,
),
),
],
),
],
),
),
),
);
}
}
Future redeemVoucher(BuildContext context) async {
String voucherCode = '';
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(context.lang.redeemVoucher),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: TextField(
onChanged: (value) {
// Convert to uppercase
setState(() {
voucherCode = value.toUpperCase();
});
},
decoration: InputDecoration(
labelText: context.lang.enterVoucherCode,
border: OutlineInputBorder(),
),
// Set the text to be uppercase
textCapitalization: TextCapitalization.characters,
),
),
],
),
);
},
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(context.lang.cancel),
),
TextButton(
onPressed: () async {
final res = await apiProvider.redeemVoucher(voucherCode);
if (!context.mounted) return;
if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.lang.voucherRedeemed)),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorCodeToText(context, res.error))),
);
}
Navigator.of(context).pop();
},
child: Text(context.lang.ok),
),
],
);
},
);
}
Future showBuyVoucher(BuildContext context) async {
int quantity = 1000;
await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(context.lang.createVoucher),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(context.lang.createVoucherDesc),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: () {
if (quantity > 1) {
setState(() {
if (quantity <= 100) return;
if (quantity <= 1000) {
quantity -= 100;
} else {
quantity -= 500;
}
});
}
},
),
Text(
NumberFormat.currency(
locale: Localizations.localeOf(context).toString(),
symbol: '',
decimalDigits: 2,
).format(quantity / 100),
style: TextStyle(fontSize: 24),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(() {
if (quantity >= 1000) {
quantity += 500;
} else {
quantity += 100;
}
});
},
),
],
),
],
),
);
},
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close the dialog
},
child: Text(context.lang.cancel),
),
TextButton(
onPressed: () async {
final res = await apiProvider.buyVoucher(quantity);
if (!context.mounted) return;
if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.lang.voucherCreated)),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorCodeToText(context, res.error))),
);
}
Navigator.of(context).pop(); // Close the dialog
},
child: Text(context.lang.buy),
),
],
);
},
);
}