mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:48:41 +00:00
vouchers work
This commit is contained in:
parent
b5a8e785ee
commit
f83cc4ace6
13 changed files with 1020 additions and 162 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
315
lib/src/views/settings/subscription/voucher_view.dart
Normal file
315
lib/src/views/settings/subscription/voucher_view.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue