Add view model to manage phone number product changes

This commit is contained in:
OmarHatem28 2022-07-04 17:15:17 +02:00
parent 8fe1a79b22
commit 7191b6d7bc
4 changed files with 208 additions and 137 deletions

View file

@ -5,6 +5,7 @@ import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart';
import 'package:cake_wallet/view_model/cake_phone/phone_plan_view_model.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/backup_service.dart';
import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_service.dart';
@ -642,5 +643,7 @@ Future setup(
getIt.registerFactoryParam<FullscreenQRPage, String, bool>( getIt.registerFactoryParam<FullscreenQRPage, String, bool>(
(String qrData, bool isLight) => FullscreenQRPage(qrData: qrData, isLight: isLight,)); (String qrData, bool isLight) => FullscreenQRPage(qrData: qrData, isLight: isLight,));
getIt.registerFactory(() => PhonePlanViewModel());
_isSetupFinished = true; _isSetupFinished = true;
} }

View file

@ -13,6 +13,7 @@ import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart';
import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/view_model/cake_phone/phone_plan_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -430,7 +431,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.phoneNumberProduct: case Routes.phoneNumberProduct:
return MaterialPageRoute<PhoneNumberProductPage>( return MaterialPageRoute<PhoneNumberProductPage>(
builder: (_) => PhoneNumberProductPage(), builder: (_) => PhoneNumberProductPage(getIt.get<PhonePlanViewModel>()),
); );
default: default:

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/cake_phone/phone_plan_view_model.dart';
import 'package:country_pickers/country.dart'; import 'package:country_pickers/country.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -10,12 +11,15 @@ import 'package:cake_wallet/entities/service_plan.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class PhoneNumberProductPage extends BasePage { class PhoneNumberProductPage extends BasePage {
PhoneNumberProductPage(); PhoneNumberProductPage(this.phonePlanViewModel);
final PhonePlanViewModel phonePlanViewModel;
@override @override
Widget body(BuildContext context) => PhoneNumberProductBody(); Widget body(BuildContext context) => PhoneNumberProductBody(phonePlanViewModel);
@override @override
Widget middle(BuildContext context) { Widget middle(BuildContext context) {
@ -31,53 +35,42 @@ class PhoneNumberProductPage extends BasePage {
} }
class PhoneNumberProductBody extends StatefulWidget { class PhoneNumberProductBody extends StatefulWidget {
PhoneNumberProductBody(); PhoneNumberProductBody(this.phonePlanViewModel);
final PhonePlanViewModel phonePlanViewModel;
@override @override
PhoneNumberProductBodyState createState() => PhoneNumberProductBodyState(); PhoneNumberProductBodyState createState() => PhoneNumberProductBodyState(phonePlanViewModel);
} }
class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> { class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
final List<ServicePlan> dummyPlans = [ PhoneNumberProductBodyState(this.phonePlanViewModel);
ServicePlan(id: "1", duration: 1, price: 20, quantity: 30),
ServicePlan(id: "2", duration: 3, price: 10, quantity: 60),
ServicePlan(id: "3", duration: 6, price: 9, quantity: 120),
ServicePlan(id: "4", duration: 12, price: 5, quantity: 200),
];
Country selectedCountry = countryList.firstWhere((element) => element.iso3Code == "USA"); final PhonePlanViewModel phonePlanViewModel;
final int rateInCents = 20;
ServicePlan selectedPlan;
@override
void initState() {
super.initState();
selectedPlan = dummyPlans.first;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: EdgeInsets.only(top: 16), padding: EdgeInsets.only(top: 16),
child: ScrollableWithBottomSection( child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.symmetric(horizontal: 24, vertical: 20).copyWith(right: 0), contentPadding: EdgeInsets.symmetric(vertical: 20),
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Padding(
S.of(context).initial_service_term, padding: const EdgeInsets.symmetric(horizontal: 24),
style: TextStyle( child: Text(
fontSize: 20, S.of(context).initial_service_term,
fontWeight: FontWeight.w700, style: TextStyle(
color: Theme.of(context).primaryTextTheme.title.color, fontSize: 20,
fontFamily: 'Lato', fontWeight: FontWeight.w700,
color: Theme.of(context).primaryTextTheme.title.color,
fontFamily: 'Lato',
),
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 8, bottom: 24, right: 24), padding: const EdgeInsets.all(24).copyWith(top: 8),
child: Text( child: Text(
S.of(context).phone_number_promotion_text, S.of(context).phone_number_promotion_text,
style: TextStyle( style: TextStyle(
@ -87,15 +80,20 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
), ),
), ),
), ),
SingleChildScrollView( Observer(builder: (_) {
scrollDirection: Axis.horizontal, return SingleChildScrollView(
child: Row( scrollDirection: Axis.horizontal,
children: dummyPlans.map((e) => planCard(e)).toList(), child: Padding(
), padding: const EdgeInsets.symmetric(horizontal: 24),
), child: Row(
children: phonePlanViewModel.servicePlans.map((e) => planCard(e)).toList(),
),
),
);
}),
const SizedBox(height: 40), const SizedBox(height: 40),
Padding( Padding(
padding: const EdgeInsets.only(right: 24), padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -115,17 +113,19 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
), ),
), ),
), ),
child: Text( child: Observer(builder: (_) {
"${selectedPlan.quantity}, " + return Text(
"${S.of(context).then} " + "${phonePlanViewModel.selectedPlan.quantity}, " +
"\$${(rateInCents / 100).toStringAsFixed(2)} " + "${S.of(context).then} " +
"${S.of(context).per_message}", "\$${(phonePlanViewModel.rateInCents / 100).toStringAsFixed(2)} " +
style: TextStyle( "${S.of(context).per_message}",
fontSize: 16, style: TextStyle(
fontWeight: FontWeight.w700, fontSize: 16,
color: Theme.of(context).primaryTextTheme.title.color, fontWeight: FontWeight.w700,
), color: Theme.of(context).primaryTextTheme.title.color,
), ),
);
}),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
@ -154,12 +154,10 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
final Country _country = country as Country; final Country _country = country as Country;
return "${_country.name} (+${_country.phoneCode})"; return "${_country.name} (+${_country.phoneCode})";
}, },
selectedAtIndex: countryList.indexOf(selectedCountry), selectedAtIndex: countryList.indexOf(phonePlanViewModel.selectedCountry),
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
onItemSelected: (Country country) { onItemSelected: (Country country) {
// TODO: update view model phonePlanViewModel.selectedCountry = country;
selectedCountry = country;
setState(() {});
}, },
images: countryList images: countryList
.map((e) => Image.asset( .map((e) => Image.asset(
@ -183,49 +181,51 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
), ),
); );
}, },
child: Row( child: Observer(builder: (_) {
children: [ return Row(
Image.asset( children: [
CountryPickerUtils.getFlagImageAssetPath(selectedCountry.isoCode), Image.asset(
height: 20.0, CountryPickerUtils.getFlagImageAssetPath(phonePlanViewModel.selectedCountry.isoCode),
width: 36.0, height: 20.0,
fit: BoxFit.fill, width: 36.0,
package: "country_pickers", fit: BoxFit.fill,
), package: "country_pickers",
Expanded( ),
child: Row( Expanded(
children: [ child: Row(
Flexible( children: [
child: Padding( Flexible(
padding: const EdgeInsets.only(left: 8, right: 6), child: Padding(
child: Text( padding: const EdgeInsets.only(left: 8, right: 6),
selectedCountry.name, child: Text(
style: TextStyle( phonePlanViewModel.selectedCountry.name,
fontSize: 16, style: TextStyle(
fontWeight: FontWeight.w700, fontSize: 16,
color: Theme.of(context).primaryTextTheme.title.color, fontWeight: FontWeight.w700,
color: Theme.of(context).primaryTextTheme.title.color,
),
), ),
), ),
), ),
), Text(
Text( "(+${phonePlanViewModel.selectedCountry.phoneCode})",
"(+${selectedCountry.phoneCode})", style: TextStyle(
style: TextStyle( fontSize: 16,
fontSize: 16, fontWeight: FontWeight.w400,
fontWeight: FontWeight.w400, color: Theme.of(context).primaryTextTheme.title.color,
color: Theme.of(context).primaryTextTheme.title.color, ),
), ),
), ],
], ),
), ),
), Icon(
Icon( Icons.arrow_forward_ios,
Icons.arrow_forward_ios, color: Theme.of(context).primaryTextTheme.title.color,
color: Theme.of(context).primaryTextTheme.title.color, size: 16,
size: 16, ),
), ],
], );
), }),
), ),
), ),
const SizedBox(height: 49), const SizedBox(height: 49),
@ -245,34 +245,42 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
), ),
child: Row( child: Row(
children: [ children: [
Container( // TODO: add action GestureDetector(
padding: const EdgeInsets.all(8), onTap: () => phonePlanViewModel.removeAdditionalSms(),
margin: const EdgeInsets.all(4), child: Container(
decoration: BoxDecoration( padding: const EdgeInsets.all(8),
color: Theme.of(context).accentTextTheme.body2.color, margin: const EdgeInsets.all(4),
shape: BoxShape.circle, decoration: BoxDecoration(
color: Theme.of(context).accentTextTheme.body2.color,
shape: BoxShape.circle,
),
child: Icon(Icons.remove, color: Colors.white, size: 15),
), ),
child: Icon(Icons.remove, color: Colors.white, size: 15),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text( child: Observer(builder: (_) {
"25", // TODO: get from view model return Text(
style: TextStyle( phonePlanViewModel.additionalSms.toString(),
fontSize: 16, style: TextStyle(
fontWeight: FontWeight.w700, fontSize: 16,
color: Theme.of(context).primaryTextTheme.title.color, fontWeight: FontWeight.w700,
), color: Theme.of(context).primaryTextTheme.title.color,
), ),
);
}),
), ),
Container( // TODO: add action GestureDetector(
padding: const EdgeInsets.all(8), onTap: () => phonePlanViewModel.addAdditionalSms(),
margin: const EdgeInsets.all(4), child: Container(
decoration: BoxDecoration( padding: const EdgeInsets.all(8),
color: Theme.of(context).accentTextTheme.body2.color, margin: const EdgeInsets.all(4),
shape: BoxShape.circle, decoration: BoxDecoration(
color: Theme.of(context).accentTextTheme.body2.color,
shape: BoxShape.circle,
),
child: Icon(Icons.add, color: Colors.white, size: 15),
), ),
child: Icon(Icons.add, color: Colors.white, size: 15),
), ),
], ],
), ),
@ -287,25 +295,27 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
bottomSectionPadding: EdgeInsets.only(bottom: 24, right: 24, left: 24), bottomSectionPadding: EdgeInsets.only(bottom: 24, right: 24, left: 24),
bottomSection: Column( bottomSection: Column(
children: <Widget>[ children: <Widget>[
RichText( Observer(builder: (_) {
text: TextSpan( return RichText(
children: [ text: TextSpan(
TextSpan(text: "${S.of(context).due_today} "), children: [
TextSpan( TextSpan(text: "${S.of(context).due_today} "),
text: "\$35.00 ", TextSpan(
style: TextStyle( text: "\$${phonePlanViewModel.totalPrice}",
fontWeight: FontWeight.w700, style: TextStyle(
color: Theme.of(context).primaryTextTheme.title.color, fontWeight: FontWeight.w700,
color: Theme.of(context).primaryTextTheme.title.color,
),
), ),
],
style: TextStyle(
fontSize: 15,
color: Theme.of(context).accentTextTheme.subhead.color,
fontFamily: 'Lato',
), ),
],
style: TextStyle(
fontSize: 15,
color: Theme.of(context).accentTextTheme.subhead.color,
fontFamily: 'Lato',
), ),
), );
), }),
const SizedBox(height: 24), const SizedBox(height: 24),
PrimaryButton( PrimaryButton(
onPressed: () {}, onPressed: () {},
@ -327,11 +337,11 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
} }
Widget planCard(ServicePlan e) { Widget planCard(ServicePlan e) {
final isSelected = phonePlanViewModel.selectedPlan == e;
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
if (e != selectedPlan) { if (!isSelected) {
selectedPlan = e; phonePlanViewModel.selectedPlan = e;
setState(() {});
} }
}, },
child: Container( child: Container(
@ -339,7 +349,7 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6), padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)), borderRadius: BorderRadius.all(Radius.circular(10)),
gradient: selectedPlan == e gradient: isSelected
? LinearGradient( ? LinearGradient(
colors: [ colors: [
Theme.of(context).primaryTextTheme.subhead.color, Theme.of(context).primaryTextTheme.subhead.color,
@ -349,7 +359,7 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
end: Alignment.bottomRight, end: Alignment.bottomRight,
) )
: null, : null,
color: selectedPlan == e ? null : Theme.of(context).primaryTextTheme.display3.decorationColor, color: isSelected ? null : Theme.of(context).primaryTextTheme.display3.decorationColor,
), ),
child: Column( child: Column(
children: [ children: [
@ -357,7 +367,7 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
"\$${e.price}/${S.of(context).month}", "\$${e.price}/${S.of(context).month}",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: selectedPlan == e ? Colors.white : Theme.of(context).primaryTextTheme.title.color, color: isSelected ? Colors.white : Theme.of(context).primaryTextTheme.title.color,
), ),
), ),
Text( Text(
@ -365,7 +375,7 @@ class PhoneNumberProductBodyState extends State<PhoneNumberProductBody> {
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: selectedPlan == e ? Colors.white : Theme.of(context).accentTextTheme.subhead.color, color: isSelected ? Colors.white : Theme.of(context).accentTextTheme.subhead.color,
fontFamily: 'Lato', fontFamily: 'Lato',
), ),
), ),

View file

@ -0,0 +1,57 @@
import 'package:cake_wallet/entities/service_plan.dart';
import 'package:country_pickers/countries.dart';
import 'package:country_pickers/country.dart';
import 'package:mobx/mobx.dart';
part 'phone_plan_view_model.g.dart';
class PhonePlanViewModel = PhonePlanViewModelBase with _$PhonePlanViewModel;
abstract class PhonePlanViewModelBase with Store {
PhonePlanViewModelBase({this.selectedPlan}) {
additionalSms = 0;
rateInCents = 20;
servicePlans = [
ServicePlan(id: "1", duration: 1, price: 20, quantity: 30),
ServicePlan(id: "2", duration: 3, price: 10, quantity: 60),
ServicePlan(id: "3", duration: 6, price: 9, quantity: 120),
ServicePlan(id: "4", duration: 12, price: 5, quantity: 200),
ServicePlan(id: "5", duration: 24, price: 2, quantity: 400),
];
// TODO: servicePlans = _getServicesFromApi
selectedPlan ??= servicePlans.first;
selectedCountry = countryList.firstWhere((element) => element.iso3Code == "USA");
}
@observable
ServicePlan selectedPlan;
@observable
Country selectedCountry;
@observable
List<ServicePlan> servicePlans;
@observable
int additionalSms;
@observable
int rateInCents;
@computed
double get totalPrice => (selectedPlan?.price ?? 0)
+ (additionalSms * ((rateInCents ?? 0) / 100)).toDouble();
@action
void addAdditionalSms() => additionalSms++;
@action
void removeAdditionalSms() {
if (additionalSms > 0) {
additionalSms--;
}
}
}