show refund

This commit is contained in:
otsmr 2025-05-10 02:13:05 +02:00
parent e7129614fa
commit 20be849641
9 changed files with 187 additions and 27 deletions

View file

@ -187,6 +187,7 @@
"open": "Offene", "open": "Offene",
"buy": "Kaufen", "buy": "Kaufen",
"createOrRedeemVoucher": "Gutschein erstellen oder einlösen", "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", "createVoucher": "Gutschein kaufen",
"createVoucherDesc": "Wähle den Wert des Gutscheins. Der Wert des Gutschein wird von deinem twonly-Guthaben abgezogen.", "createVoucherDesc": "Wähle den Wert des Gutscheins. Der Wert des Gutschein wird von deinem twonly-Guthaben abgezogen.",
"redeemVoucher": "Gutschein einlösen", "redeemVoucher": "Gutschein einlösen",
@ -199,6 +200,7 @@
"transactionCash": "Bargeldtransaktion", "transactionCash": "Bargeldtransaktion",
"transactionPlanUpgrade": "Planupgrade", "transactionPlanUpgrade": "Planupgrade",
"transactionRefund": "Rückerstattung", "transactionRefund": "Rückerstattung",
"refund": "Rückerstattung",
"transactionThanksForTesting": "Danke fürs Testen", "transactionThanksForTesting": "Danke fürs Testen",
"transactionUnknown": "Unbekannte Transaktion", "transactionUnknown": "Unbekannte Transaktion",
"transactionVoucherCreated": "Gutschein erstellt", "transactionVoucherCreated": "Gutschein erstellt",

View file

@ -356,6 +356,7 @@
"requestedVouchers": "Requested vouchers", "requestedVouchers": "Requested vouchers",
"redeemedVouchers": "Redeemed vouchers", "redeemedVouchers": "Redeemed vouchers",
"buy": "Buy", "buy": "Buy",
"subscriptionRefund": "When you upgrade, you will receive a refund of {refund} for your current subscription.",
"transactionCash": "Cash transaction", "transactionCash": "Cash transaction",
"transactionPlanUpgrade": "Plan upgrade", "transactionPlanUpgrade": "Plan upgrade",
"transactionRefund": "Refund transaction", "transactionRefund": "Refund transaction",
@ -364,6 +365,7 @@
"transactionVoucherCreated": "Voucher created", "transactionVoucherCreated": "Voucher created",
"transactionVoucherRedeemed": "Voucher redeemed", "transactionVoucherRedeemed": "Voucher redeemed",
"checkoutOptions": "Options", "checkoutOptions": "Options",
"refund": "Refund",
"checkoutPayYearly": "Pay yearly", "checkoutPayYearly": "Pay yearly",
"checkoutTotal": "Total", "checkoutTotal": "Total",
"selectPaymentMethode": "Select Payment Method", "selectPaymentMethode": "Select Payment Method",

View file

@ -1199,6 +1199,12 @@ abstract class AppLocalizations {
/// **'Buy'** /// **'Buy'**
String get 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. /// No description provided for @transactionCash.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1247,6 +1253,12 @@ abstract class AppLocalizations {
/// **'Options'** /// **'Options'**
String get checkoutOptions; String get checkoutOptions;
/// No description provided for @refund.
///
/// In en, this message translates to:
/// **'Refund'**
String get refund;
/// No description provided for @checkoutPayYearly. /// No description provided for @checkoutPayYearly.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -572,6 +572,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get buy => 'Kaufen'; 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 @override
String get transactionCash => 'Bargeldtransaktion'; String get transactionCash => 'Bargeldtransaktion';
@ -596,6 +601,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get checkoutOptions => 'Optionen'; String get checkoutOptions => 'Optionen';
@override
String get refund => 'Rückerstattung';
@override @override
String get checkoutPayYearly => 'Jährlich bezahlen'; String get checkoutPayYearly => 'Jährlich bezahlen';

View file

@ -572,6 +572,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get buy => 'Buy'; 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 @override
String get transactionCash => 'Cash transaction'; String get transactionCash => 'Cash transaction';
@ -596,6 +601,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get checkoutOptions => 'Options'; String get checkoutOptions => 'Options';
@override
String get refund => 'Refund';
@override @override
String get checkoutPayYearly => 'Pay yearly'; String get checkoutPayYearly => 'Pay yearly';

View file

@ -0,0 +1,15 @@
import 'package:flutter/widgets.dart';
class AdditionalUsersView extends StatefulWidget {
const AdditionalUsersView({super.key});
@override
State<AdditionalUsersView> createState() => _AdditionalUsersViewState();
}
class _AdditionalUsersViewState extends State<AdditionalUsersView> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View file

@ -4,12 +4,15 @@ import 'package:twonly/src/views/settings/subscription/select_payment.dart';
import 'package:twonly/src/views/settings/subscription/subscription_view.dart'; import 'package:twonly/src/views/settings/subscription/subscription_view.dart';
class CheckoutView extends StatefulWidget { class CheckoutView extends StatefulWidget {
const CheckoutView({ const CheckoutView(
super.key, {super.key,
required this.planId, required this.planId,
}); this.refund,
this.disableMonthlyOption});
final String planId; final String planId;
final int? refund;
final bool? disableMonthlyOption;
@override @override
State<CheckoutView> createState() => _CheckoutViewState(); State<CheckoutView> createState() => _CheckoutViewState();
@ -49,28 +52,53 @@ class _CheckoutViewState extends State<CheckoutView> {
child: ListView( child: ListView(
children: [ children: [
PlanCard(planId: widget.planId), PlanCard(planId: widget.planId),
Padding( if (widget.disableMonthlyOption == null ||
padding: const EdgeInsets.all(16.0), !widget.disableMonthlyOption!)
child: ListTile( Padding(
title: Text(context.lang.checkoutPayYearly), padding: const EdgeInsets.all(16.0),
onTap: () { child: ListTile(
paidMonthly = !paidMonthly; title: Text(context.lang.checkoutPayYearly),
setCheckout(false); onTap: () {
},
trailing: Checkbox(
value: !paidMonthly,
onChanged: (a) {
paidMonthly = !paidMonthly; paidMonthly = !paidMonthly;
setCheckout(false); 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(
padding: EdgeInsets.all(16), padding: EdgeInsets.symmetric(horizontal: 16),
child: Card( child: Card(
child: Padding( child: Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
@ -90,6 +118,7 @@ class _CheckoutViewState extends State<CheckoutView> {
), ),
), ),
), ),
SizedBox(height: 16),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 16), padding: EdgeInsets.symmetric(horizontal: 16),
child: FilledButton( child: FilledButton(
@ -97,7 +126,10 @@ class _CheckoutViewState extends State<CheckoutView> {
bool? success = await Navigator.push(context, bool? success = await Navigator.push(context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return SelectPaymentView( return SelectPaymentView(
planId: widget.planId, payMonthly: paidMonthly); planId: widget.planId,
payMonthly: paidMonthly,
refund: widget.refund,
);
})); }));
if (success != null && success && context.mounted) { if (success != null && success && context.mounted) {
Navigator.pop(context); Navigator.pop(context);

View file

@ -10,16 +10,17 @@ import 'package:twonly/src/views/settings/subscription/voucher_view.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class SelectPaymentView extends StatefulWidget { class SelectPaymentView extends StatefulWidget {
const SelectPaymentView({ const SelectPaymentView(
super.key, {super.key,
this.planId, this.planId,
this.payMonthly, this.payMonthly,
this.valueInCents, this.valueInCents,
}); this.refund});
final String? planId; final String? planId;
final bool? payMonthly; final bool? payMonthly;
final int? valueInCents; final int? valueInCents;
final int? refund;
@override @override
State<SelectPaymentView> createState() => _SelectPaymentViewState(); State<SelectPaymentView> createState() => _SelectPaymentViewState();
@ -162,8 +163,31 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
), ),
), ),
), ),
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(
padding: EdgeInsets.all(16), padding: EdgeInsets.symmetric(horizontal: 16),
child: Card( child: Card(
child: Padding( child: Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
@ -183,6 +207,7 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
), ),
), ),
), ),
SizedBox(height: 16),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 16), padding: EdgeInsets.symmetric(horizontal: 16),
child: FilledButton( child: FilledButton(
@ -199,6 +224,7 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
user.subscriptionPlan = widget.planId!; user.subscriptionPlan = widget.planId!;
await updateUser(user); await updateUser(user);
} }
if (!context.mounted) return;
context context
.read<CustomChangeProvider>() .read<CustomChangeProvider>()
.updatePlan(widget.planId!); .updatePlan(widget.planId!);

View file

@ -51,6 +51,33 @@ Future<Response_PlanBallance?> loadPlanBallance() async {
return ballance; 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 { class SubscriptionView extends StatefulWidget {
const SubscriptionView({super.key}); const SubscriptionView({super.key});
@ -94,6 +121,10 @@ class _SubscriptionViewState extends State<SubscriptionView> {
} }
String currentPlan = context.read<CustomChangeProvider>().plan; String currentPlan = context.read<CustomChangeProvider>().plan;
int refund = 0;
if (currentPlan == "Pro" && ballance != null) {
refund = calculateRefund(ballance!);
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -138,7 +169,9 @@ class _SubscriptionViewState extends State<SubscriptionView> {
onTap: () async { onTap: () async {
await Navigator.push(context, await Navigator.push(context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return CheckoutView(planId: "Pro"); return CheckoutView(
planId: "Pro",
);
})); }));
initAsync(); initAsync();
}, },
@ -146,10 +179,17 @@ class _SubscriptionViewState extends State<SubscriptionView> {
if (currentPlan != "Family") if (currentPlan != "Family")
PlanCard( PlanCard(
planId: "Family", planId: "Family",
refund: refund,
onTap: () async { onTap: () async {
await Navigator.push(context, await Navigator.push(context,
MaterialPageRoute(builder: (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(); initAsync();
}, },
@ -243,10 +283,12 @@ int getPlanPrice(String planId, bool paidMonthly) {
class PlanCard extends StatelessWidget { class PlanCard extends StatelessWidget {
final String planId; final String planId;
final Function()? onTap; final Function()? onTap;
final int? refund;
const PlanCard({ const PlanCard({
super.key, super.key,
required this.planId, required this.planId,
this.refund,
this.onTap, 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,
),
),
)
], ],
), ),
), ),