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",
"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",

View file

@ -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",

View file

@ -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:

View file

@ -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';

View file

@ -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';

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';
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<CheckoutView> createState() => _CheckoutViewState();
@ -49,28 +52,53 @@ class _CheckoutViewState extends State<CheckoutView> {
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<CheckoutView> {
),
),
),
SizedBox(height: 16),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: FilledButton(
@ -97,7 +126,10 @@ class _CheckoutViewState extends State<CheckoutView> {
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);

View file

@ -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<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: EdgeInsets.all(16),
padding: EdgeInsets.symmetric(horizontal: 16),
child: Card(
child: Padding(
padding: EdgeInsets.all(16),
@ -183,6 +207,7 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
),
),
),
SizedBox(height: 16),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: FilledButton(
@ -199,6 +224,7 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
user.subscriptionPlan = widget.planId!;
await updateUser(user);
}
if (!context.mounted) return;
context
.read<CustomChangeProvider>()
.updatePlan(widget.planId!);

View file

@ -51,6 +51,33 @@ Future<Response_PlanBallance?> 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<SubscriptionView> {
}
String currentPlan = context.read<CustomChangeProvider>().plan;
int refund = 0;
if (currentPlan == "Pro" && ballance != null) {
refund = calculateRefund(ballance!);
}
return Scaffold(
appBar: AppBar(
@ -138,7 +169,9 @@ class _SubscriptionViewState extends State<SubscriptionView> {
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<SubscriptionView> {
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,
),
),
)
],
),
),