diff --git a/lib/app.dart b/lib/app.dart index e4e7a72..baa70ea 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -41,20 +41,25 @@ class _AppState extends State with WidgetsBindingObserver { // register global callbacks to the widget tree globalCallbackConnectionState = (update) { context.read().updateConnectionState(update); + setUserPlan(); setupNotificationWithUsers(); }; initAsync(); } - Future initAsync() async { - apiProvider.connect(); + Future setUserPlan() async { final user = await getUser(); if (user != null && context.mounted) { context.read().updatePlan(user.subscriptionPlan); } } + Future initAsync() async { + setUserPlan(); + apiProvider.connect(); + } + @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index a811b57..8865f52 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -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" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index a9445dc..9ea2909 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -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" } \ 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 036b24a..9c4ff28 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -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 { diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 27eeb2a..5809dae 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -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'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index ee91792..972e38d 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -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'; } diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index cb8c49d..200586c 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -419,6 +419,34 @@ class ApiProvider { return null; } + Future 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 buyVoucher(int valueInCents) async { + var get = ApplicationData_CreateVoucher()..valueCents = valueInCents; + var appData = ApplicationData()..createvoucher = get; + var req = createClientToServerFromApplicationData(appData); + return await sendRequestSync(req); + } + + Future redeemVoucher(String voucher) async { + var get = ApplicationData_RedeemVoucher()..voucher = voucher; + var appData = ApplicationData()..redeemvoucher = get; + var req = createClientToServerFromApplicationData(appData); + return await sendRequestSync(req); + } + Future updateFCMToken(String googleFcm) async { var get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm; var appData = ApplicationData()..updategooglefcmtoken = get; diff --git a/lib/src/providers/connection_provider.dart b/lib/src/providers/connection_provider.dart index 68586e2..d16e04a 100644 --- a/lib/src/providers/connection_provider.dart +++ b/lib/src/providers/connection_provider.dart @@ -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 updateConnectionState(bool update) async { _isConnected = update; notifyListeners(); diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 5b57f5f..0052af4 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -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 } diff --git a/lib/src/views/onboarding/onboarding_view.dart b/lib/src/views/onboarding/onboarding_view.dart index 7209dc5..889fbfc 100644 --- a/lib/src/views/onboarding/onboarding_view.dart +++ b/lib/src/views/onboarding/onboarding_view.dart @@ -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; diff --git a/lib/src/views/onboarding/register_view.dart b/lib/src/views/onboarding/register_view.dart index 56cabde..944168e 100644 --- a/lib/src/views/onboarding/register_view.dart +++ b/lib/src/views/onboarding/register_view.dart @@ -37,6 +37,10 @@ class _RegisterViewState extends State { 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 { 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 { 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 diff --git a/lib/src/views/settings/subscription/subscription_view.dart b/lib/src/views/settings/subscription/subscription_view.dart index 06bbbcc..095dcc5 100644 --- a/lib/src/views/settings/subscription/subscription_view.dart +++ b/lib/src/views/settings/subscription/subscription_view.dart @@ -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 { - bool hasInternet = true; + bool loaded = false; int ballanceInCents = 0; + DateTime? nextPayment; @override void initState() { @@ -26,13 +29,16 @@ class _SubscriptionViewState extends State { } 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 { @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().plan; + return Scaffold( appBar: AppBar( title: Text(context.lang.settingsSubscription), ), - body: Column( + body: ListView( children: [ Padding( padding: const EdgeInsets.all(32.0), @@ -62,73 +71,119 @@ class _SubscriptionViewState extends State { ), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 3), child: Text( - context.watch().plan, + currentPlan, style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, - color: Colors.black, + color: isDarkMode(context) ? Colors.black : Colors.white, ), ), ), ), ), - Expanded( - child: ListView( - children: [ - Center( - child: Text("Upgrade your current plan."), + if (currentPlan != "Family" && currentPlan != "Pro") + Center( + child: Padding( + padding: const EdgeInsets.all(18.0), + child: Text( + context.lang.upgradeToPaidPlan, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), ), - SizedBox(height: 10), - PlanCard( - title: 'Pro', - yearlyPrice: '10€/year', - monthlyPrice: '1€/month', - features: [ - '✓ Unlimited media files', - '1 additional Plus user', - '3 additional Free users', - ], - ), - SizedBox(height: 10), - PlanCard( - title: 'Family', - yearlyPrice: '20€/year', - monthlyPrice: '2€/month', - features: [ - '✓ All from Pro', - '4 additional Plus users', - '5 additional Free users', - ], - ), - SizedBox(height: 10), - Divider(), - BetterListTile( - icon: FontAwesomeIcons.ticket, - text: "Redeem code for additional user", - onTap: () {}, - ), - BetterListTile( - icon: FontAwesomeIcons.moneyBillTransfer, - text: "Your transaction history", - subtitle: Text("Current ballance: $formattedBalance"), - onTap: () {}, - ), - BetterListTile( - icon: FontAwesomeIcons.userPlus, - text: "Manage your additional users", - subtitle: Text("Open: 3"), - onTap: () {}, - ), - BetterListTile( - icon: FontAwesomeIcons.gift, - text: "Create or redeem voucher", - onTap: () {}, - ), - SizedBox(height: 30) - ], - // tranaction + ), ), - ) + if (currentPlan != "Family" && currentPlan != "Pro") + PlanCard( + title: "Pro", + yearlyPrice: context.lang.proYearlyPrice, + monthlyPrice: context.lang.proMonthlyPrice, + features: [ + context.lang.proFeature1, + context.lang.proFeature2, + context.lang.proFeature3, + ], + ), + if (currentPlan != "Family") + PlanCard( + title: "Family", + yearlyPrice: context.lang.familyYearlyPrice, + monthlyPrice: context.lang.familyMonthlyPrice, + features: [ + context.lang.familyFeature1, + context.lang.familyFeature2, + context.lang.familyFeature3, + ], + ), + if (currentPlan == "Preview" || currentPlan == "Free") ...[ + SizedBox(height: 10), + 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.userPlus, + text: "Manage your subscription", + subtitle: (nextPayment != null) + ? Text( + "Next payment: ${DateFormat.yMMMMd(myLocale.toString()).format(nextPayment!)}") + : null, + onTap: () {}, + ), + BetterListTile( + icon: FontAwesomeIcons.moneyBillTransfer, + text: context.lang.transactionHistory, + subtitle: (loaded) + ? Text("${context.lang.currentBalance}: $formattedBalance") + : null, + onTap: () {}, + ), + if (currentPlan == "Family" || currentPlan == "Pro") + BetterListTile( + icon: FontAwesomeIcons.userPlus, + text: context.lang.manageAdditionalUsers, + subtitle: (loaded) ? Text("${context.lang.open}: 3") : null, + onTap: () {}, + ), + BetterListTile( + icon: FontAwesomeIcons.ticket, + text: context.lang.createOrRedeemVoucher, + onTap: () async { + await Navigator.push(context, + MaterialPageRoute(builder: (context) { + return VoucherView(); + })); + initAsync(); + }, + ), + SizedBox(height: 30) ], ), ); @@ -175,27 +230,28 @@ class PlanCard extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - SizedBox(height: 10), - Column( - children: [ - Text( - yearlyPrice, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, + if (yearlyPrice != "") SizedBox(height: 10), + if (yearlyPrice != "") + Column( + children: [ + Text( + yearlyPrice, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), - ), - Text( - monthlyPrice, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Colors.grey, + Text( + monthlyPrice, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), ), - ), - ], - ) + ], + ) ], ), SizedBox(height: 10), diff --git a/lib/src/views/settings/subscription/voucher_view.dart b/lib/src/views/settings/subscription/voucher_view.dart new file mode 100644 index 0000000..d671b0b --- /dev/null +++ b/lib/src/views/settings/subscription/voucher_view.dart @@ -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 createState() => _VoucherViewState(); +} + +class _VoucherViewState extends State { + List 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 createState() => _VoucherCardState(); +} + +class _VoucherCardState extends State { + 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), + ), + ], + ); + }, + ); +}