diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 227498c..366389f 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -187,6 +187,7 @@ "open": "Offene", "buy": "Kaufen", "createOrRedeemVoucher": "Gutschein erstellen oder einlösen", + "subscriptionRefund": "Wenn du ein Upgrade durchführst, erhältst du eine Rückerstattung von {refund} für dein aktuelles Abonnement.", "createVoucher": "Gutschein kaufen", "createVoucherDesc": "Wähle den Wert des Gutscheins. Der Wert des Gutschein wird von deinem twonly-Guthaben abgezogen.", "redeemVoucher": "Gutschein einlösen", @@ -199,6 +200,7 @@ "transactionCash": "Bargeldtransaktion", "transactionPlanUpgrade": "Planupgrade", "transactionRefund": "Rückerstattung", + "refund": "Rückerstattung", "transactionThanksForTesting": "Danke fürs Testen", "transactionUnknown": "Unbekannte Transaktion", "transactionVoucherCreated": "Gutschein erstellt", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 325d698..09b74f7 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -356,6 +356,7 @@ "requestedVouchers": "Requested vouchers", "redeemedVouchers": "Redeemed vouchers", "buy": "Buy", + "subscriptionRefund": "When you upgrade, you will receive a refund of {refund} for your current subscription.", "transactionCash": "Cash transaction", "transactionPlanUpgrade": "Plan upgrade", "transactionRefund": "Refund transaction", @@ -364,6 +365,7 @@ "transactionVoucherCreated": "Voucher created", "transactionVoucherRedeemed": "Voucher redeemed", "checkoutOptions": "Options", + "refund": "Refund", "checkoutPayYearly": "Pay yearly", "checkoutTotal": "Total", "selectPaymentMethode": "Select Payment Method", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 30480a5..d4e4a22 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1199,6 +1199,12 @@ abstract class AppLocalizations { /// **'Buy'** String get buy; + /// No description provided for @subscriptionRefund. + /// + /// In en, this message translates to: + /// **'When you upgrade, you will receive a refund of {refund} for your current subscription.'** + String subscriptionRefund(Object refund); + /// No description provided for @transactionCash. /// /// In en, this message translates to: @@ -1247,6 +1253,12 @@ abstract class AppLocalizations { /// **'Options'** String get checkoutOptions; + /// No description provided for @refund. + /// + /// In en, this message translates to: + /// **'Refund'** + String get refund; + /// No description provided for @checkoutPayYearly. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index b611047..8ed9f88 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -572,6 +572,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get buy => 'Kaufen'; + @override + String subscriptionRefund(Object refund) { + return 'Wenn du ein Upgrade durchführst, erhältst du eine Rückerstattung von $refund für dein aktuelles Abonnement.'; + } + @override String get transactionCash => 'Bargeldtransaktion'; @@ -596,6 +601,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get checkoutOptions => 'Optionen'; + @override + String get refund => 'Rückerstattung'; + @override String get checkoutPayYearly => 'Jährlich bezahlen'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index b959d5c..6d05549 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -572,6 +572,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get buy => 'Buy'; + @override + String subscriptionRefund(Object refund) { + return 'When you upgrade, you will receive a refund of $refund for your current subscription.'; + } + @override String get transactionCash => 'Cash transaction'; @@ -596,6 +601,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get checkoutOptions => 'Options'; + @override + String get refund => 'Refund'; + @override String get checkoutPayYearly => 'Pay yearly'; diff --git a/lib/src/views/settings/subscription/additional_users_view.dart b/lib/src/views/settings/subscription/additional_users_view.dart new file mode 100644 index 0000000..524ad2a --- /dev/null +++ b/lib/src/views/settings/subscription/additional_users_view.dart @@ -0,0 +1,15 @@ +import 'package:flutter/widgets.dart'; + +class AdditionalUsersView extends StatefulWidget { + const AdditionalUsersView({super.key}); + + @override + State createState() => _AdditionalUsersViewState(); +} + +class _AdditionalUsersViewState extends State { + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/src/views/settings/subscription/checkout_view.dart b/lib/src/views/settings/subscription/checkout_view.dart index 61d2a70..1b83c42 100644 --- a/lib/src/views/settings/subscription/checkout_view.dart +++ b/lib/src/views/settings/subscription/checkout_view.dart @@ -4,12 +4,15 @@ import 'package:twonly/src/views/settings/subscription/select_payment.dart'; import 'package:twonly/src/views/settings/subscription/subscription_view.dart'; class CheckoutView extends StatefulWidget { - const CheckoutView({ - super.key, - required this.planId, - }); + const CheckoutView( + {super.key, + required this.planId, + this.refund, + this.disableMonthlyOption}); final String planId; + final int? refund; + final bool? disableMonthlyOption; @override State createState() => _CheckoutViewState(); @@ -49,28 +52,53 @@ class _CheckoutViewState extends State { child: ListView( children: [ PlanCard(planId: widget.planId), - Padding( - padding: const EdgeInsets.all(16.0), - child: ListTile( - title: Text(context.lang.checkoutPayYearly), - onTap: () { - paidMonthly = !paidMonthly; - setCheckout(false); - }, - trailing: Checkbox( - value: !paidMonthly, - onChanged: (a) { + if (widget.disableMonthlyOption == null || + !widget.disableMonthlyOption!) + Padding( + padding: const EdgeInsets.all(16.0), + child: ListTile( + title: Text(context.lang.checkoutPayYearly), + onTap: () { paidMonthly = !paidMonthly; setCheckout(false); }, + trailing: Checkbox( + value: !paidMonthly, + onChanged: (a) { + paidMonthly = !paidMonthly; + setCheckout(false); + }, + ), ), ), - ), ], ), ), + if (widget.refund != null) + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Card( + child: Padding( + padding: EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + context.lang.refund, + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + "+${localePrizing(context, widget.refund!)}", + textAlign: TextAlign.end, + style: TextStyle(color: context.color.primary), + ), + ], + ), + ), + ), + ), Padding( - padding: EdgeInsets.all(16), + padding: EdgeInsets.symmetric(horizontal: 16), child: Card( child: Padding( padding: EdgeInsets.all(16), @@ -90,6 +118,7 @@ class _CheckoutViewState extends State { ), ), ), + SizedBox(height: 16), Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: FilledButton( @@ -97,7 +126,10 @@ class _CheckoutViewState extends State { bool? success = await Navigator.push(context, MaterialPageRoute(builder: (context) { return SelectPaymentView( - planId: widget.planId, payMonthly: paidMonthly); + planId: widget.planId, + payMonthly: paidMonthly, + refund: widget.refund, + ); })); if (success != null && success && context.mounted) { Navigator.pop(context); diff --git a/lib/src/views/settings/subscription/select_payment.dart b/lib/src/views/settings/subscription/select_payment.dart index b40af5e..61e95e6 100644 --- a/lib/src/views/settings/subscription/select_payment.dart +++ b/lib/src/views/settings/subscription/select_payment.dart @@ -10,16 +10,17 @@ import 'package:twonly/src/views/settings/subscription/voucher_view.dart'; import 'package:url_launcher/url_launcher.dart'; class SelectPaymentView extends StatefulWidget { - const SelectPaymentView({ - super.key, - this.planId, - this.payMonthly, - this.valueInCents, - }); + const SelectPaymentView( + {super.key, + this.planId, + this.payMonthly, + this.valueInCents, + this.refund}); final String? planId; final bool? payMonthly; final int? valueInCents; + final int? refund; @override State createState() => _SelectPaymentViewState(); @@ -162,8 +163,31 @@ class _SelectPaymentViewState extends State { ), ), ), + if (widget.refund != null) + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Card( + child: Padding( + padding: EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + context.lang.refund, + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + "+${localePrizing(context, widget.refund!)}", + textAlign: TextAlign.end, + style: TextStyle(color: context.color.primary), + ), + ], + ), + ), + ), + ), Padding( - padding: EdgeInsets.all(16), + padding: EdgeInsets.symmetric(horizontal: 16), child: Card( child: Padding( padding: EdgeInsets.all(16), @@ -183,6 +207,7 @@ class _SelectPaymentViewState extends State { ), ), ), + SizedBox(height: 16), Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: FilledButton( @@ -199,6 +224,7 @@ class _SelectPaymentViewState extends State { user.subscriptionPlan = widget.planId!; await updateUser(user); } + if (!context.mounted) return; context .read() .updatePlan(widget.planId!); diff --git a/lib/src/views/settings/subscription/subscription_view.dart b/lib/src/views/settings/subscription/subscription_view.dart index 6ca78dd..7cef705 100644 --- a/lib/src/views/settings/subscription/subscription_view.dart +++ b/lib/src/views/settings/subscription/subscription_view.dart @@ -51,6 +51,33 @@ Future loadPlanBallance() async { return ballance; } +// ignore: constant_identifier_names +const int MONTHLY_PAYMENT_DAYS = 30; +// ignore: constant_identifier_names +const int YEARLY_PAYMENT_DAYS = 365; + +int calculateRefund(Response_PlanBallance current) { + int refund = getPlanPrice("Pro", true); + + if (current.paymentPeriodDays == YEARLY_PAYMENT_DAYS) { + final elapsedDays = DateTime.now() + .difference(DateTime.fromMillisecondsSinceEpoch( + current.lastPaymentDoneUnixTimestamp.toInt() * 1000)) + .inDays; + if (elapsedDays < current.paymentPeriodDays.toInt()) { + // User has yearly plan with 10€ + // used it half a year and wants now to upgrade => gets 5€ discount... + // math.ceil(((365-(365/2))/365)*10) + // => 5€ + + refund = (((YEARLY_PAYMENT_DAYS - elapsedDays) / YEARLY_PAYMENT_DAYS) * + getPlanPrice("Pro", false)) + .ceil(); + } + } + return refund; +} + class SubscriptionView extends StatefulWidget { const SubscriptionView({super.key}); @@ -94,6 +121,10 @@ class _SubscriptionViewState extends State { } String currentPlan = context.read().plan; + int refund = 0; + if (currentPlan == "Pro" && ballance != null) { + refund = calculateRefund(ballance!); + } return Scaffold( appBar: AppBar( @@ -138,7 +169,9 @@ class _SubscriptionViewState extends State { onTap: () async { await Navigator.push(context, MaterialPageRoute(builder: (context) { - return CheckoutView(planId: "Pro"); + return CheckoutView( + planId: "Pro", + ); })); initAsync(); }, @@ -146,10 +179,17 @@ class _SubscriptionViewState extends State { if (currentPlan != "Family") PlanCard( planId: "Family", + refund: refund, onTap: () async { await Navigator.push(context, MaterialPageRoute(builder: (context) { - return CheckoutView(planId: "Family"); + return CheckoutView( + planId: "Family", + refund: (refund > 0) ? refund : null, + disableMonthlyOption: (currentPlan == "Pro" && + ballance!.paymentPeriodDays.toInt() == + YEARLY_PAYMENT_DAYS), + ); })); initAsync(); }, @@ -243,10 +283,12 @@ int getPlanPrice(String planId, bool paidMonthly) { class PlanCard extends StatelessWidget { final String planId; final Function()? onTap; + final int? refund; const PlanCard({ super.key, required this.planId, + this.refund, this.onTap, }); @@ -340,6 +382,19 @@ class PlanCard extends StatelessWidget { ), ), ), + if (refund != null && refund! > 0) + Padding( + padding: const EdgeInsets.only(top: 7), + child: Text( + context.lang + .subscriptionRefund(localePrizing(context, refund!)), + textAlign: TextAlign.center, + style: TextStyle( + color: context.color.primary, + fontSize: 12, + ), + ), + ) ], ), ),