From 9000974fb6afd1730ebaef23cfb2acad23bd8069 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 14 Apr 2022 18:31:24 +0530 Subject: [PATCH 01/14] UserService: Support to fetch user/details/v2 --- lib/models/user_details.dart | 58 ++++++++++++++++++++++++++++++++++ lib/services/user_service.dart | 18 +++++++++++ 2 files changed, 76 insertions(+) diff --git a/lib/models/user_details.dart b/lib/models/user_details.dart index 94c919904..3ae0693c9 100644 --- a/lib/models/user_details.dart +++ b/lib/models/user_details.dart @@ -6,6 +6,7 @@ class UserDetails { final int fileCount; final int sharedCollectionsCount; final Subscription subscription; + final FamilyData familyData; UserDetails( this.email, @@ -13,6 +14,7 @@ class UserDetails { this.fileCount, this.sharedCollectionsCount, this.subscription, + this.familyData, ); factory UserDetails.fromMap(Map map) { @@ -22,6 +24,7 @@ class UserDetails { map['fileCount'] as int, map['sharedCollectionsCount'] as int, Subscription.fromMap(map['subscription']), + FamilyData.fromMap(map['familyData']), ); } @@ -32,6 +35,61 @@ class UserDetails { 'fileCount': fileCount, 'sharedCollectionsCount': sharedCollectionsCount, 'subscription': subscription, + 'familyData': familyData + }; + } +} + +class FamilyMember { + final String email; + final int usage; + final String id; + final bool isAdmin; + + FamilyMember(this.email, this.usage, this.id, this.isAdmin); + + factory FamilyMember.fromMap(Map map) { + return FamilyMember( + (map['email'] ?? '') as String, + map['usage'] as int, + map['id'] as String, + map['isAdmin'] as bool, + ); + } + + Map toMap() { + return {'email': email, 'usage': usage, 'id': id, 'isAdmin': isAdmin}; + } +} + +class FamilyData { + final List members; + + // Storage available based on the family plan + final int storage; + final int expiry; + + FamilyData(this.members, this.storage, this.expiry); + + factory FamilyData.fromMap(Map map) { + if (map == null) { + return null; + } + assert(map['members'] != null && map['members'].length >= 0); + final members = List.from( + map['members'].map((x) => FamilyMember.fromMap(x))); + return FamilyData( + members, + map['storage'] as int, + map['expiry'] as int, + ); + } + + Map toMap() { + return { + 'members': members.map((x) => x?.toMap())?.toList(), + 'storage': storage, + 'expiry': expiry }; } } diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 3a073b16e..9ab4e89dc 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -121,6 +121,24 @@ class UserService { } } + Future getUserDetailsV2({bool memberCount = true}) async { + try { + final response = await _dio.get( + _config.getHttpEndpoint() + + "/users/details/v2?memoryCount=$memberCount", + options: Options( + headers: { + "X-Auth-Token": _config.getToken(), + }, + ), + ); + return UserDetails.fromMap(response.data); + } on DioError catch (e) { + _logger.info(e); + rethrow; + } + } + Future getActiveSessions() async { try { final response = await _dio.get( From 6dcb4ba0b438d7b4b03fd749cf46022806570306 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 14 Apr 2022 19:47:41 +0530 Subject: [PATCH 02/14] switch to usageDetailsV2 for fetching subscription --- lib/models/user_details.dart | 8 ++-- lib/ui/payment/stripe_subscription_page.dart | 26 +++++------ lib/ui/payment/subscription.dart | 2 +- .../payment/subscription_common_widgets.dart | 26 +++-------- lib/ui/payment/subscription_page.dart | 44 +++++++++---------- 5 files changed, 45 insertions(+), 61 deletions(-) diff --git a/lib/models/user_details.dart b/lib/models/user_details.dart index 3ae0693c9..a1b237ad7 100644 --- a/lib/models/user_details.dart +++ b/lib/models/user_details.dart @@ -67,9 +67,9 @@ class FamilyData { // Storage available based on the family plan final int storage; - final int expiry; + final int expiryTime; - FamilyData(this.members, this.storage, this.expiry); + FamilyData(this.members, this.storage, this.expiryTime); factory FamilyData.fromMap(Map map) { if (map == null) { @@ -81,7 +81,7 @@ class FamilyData { return FamilyData( members, map['storage'] as int, - map['expiry'] as int, + map['expiryTime'] as int, ); } @@ -89,7 +89,7 @@ class FamilyData { return { 'members': members.map((x) => x?.toMap())?.toList(), 'storage': storage, - 'expiry': expiry + 'expiryTime': expiryTime }; } } diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index 96a46c12f..1e46766b4 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -7,6 +7,7 @@ import 'package:logging/logging.dart'; import 'package:photos/models/billing_plan.dart'; import 'package:photos/models/subscription.dart'; import 'package:photos/services/billing_service.dart'; +import 'package:photos/services/user_service.dart'; import 'package:photos/ui/common/dialogs.dart'; import 'package:photos/ui/loading_widget.dart'; import 'package:photos/ui/payment/payment_web_page.dart'; @@ -34,9 +35,10 @@ class StripeSubscriptionPage extends StatefulWidget { class _StripeSubscriptionPageState extends State { final _logger = Logger("StripeSubscriptionPage"); final _billingService = BillingService.instance; + final _userService = UserService.instance; Subscription _currentSubscription; ProgressDialog _dialog; - Future _usageFuture; + int _usage; // indicates if user's subscription plan is still active bool _hasActiveSubscription; @@ -54,12 +56,12 @@ class _StripeSubscriptionPageState extends State { } Future _fetchSub() async { - return _billingService.fetchSubscription().then((subscription) async { - _currentSubscription = subscription; + return _userService.getUserDetailsV2().then((userDetails) async { + _currentSubscription = userDetails.subscription; _showYearlyPlan = _currentSubscription.isYearlyPlan(); _hasActiveSubscription = _currentSubscription.isValid(); _isStripeSubscriber = _currentSubscription.paymentProvider == kStripe; - _usageFuture = _billingService.fetchUsage(); + _usage = userDetails.usage; return _filterStripeForUI().then((value) { _hasLoadedData = true; setState(() {}); @@ -128,7 +130,7 @@ class _StripeSubscriptionPageState extends State { widgets.add(SubscriptionHeaderWidget( isOnboarding: widget.isOnboarding, - usageFuture: _usageFuture, + currentUsage: _usage, )); widgets.addAll([ @@ -308,16 +310,12 @@ class _StripeSubscriptionPageState extends State { "please cancel your existing subscription from ${_currentSubscription.paymentProvider} first"); return; } - await _dialog.show(); - if (_usageFuture != null) { - final usage = await _usageFuture; - await _dialog.hide(); - if (usage > plan.storage) { - showErrorDialog( - context, "sorry", "you cannot downgrade to this plan"); - return; - } + if (_usage > plan.storage) { + showErrorDialog( + context, "sorry", "you cannot downgrade to this plan"); + return; } + await _dialog.show(); String stripPurChaseAction = 'buy'; if (_isStripeSubscriber && _hasActiveSubscription) { // confirm if user wants to change plan or not diff --git a/lib/ui/payment/subscription.dart b/lib/ui/payment/subscription.dart index 7beb8a66a..7bef46e59 100644 --- a/lib/ui/payment/subscription.dart +++ b/lib/ui/payment/subscription.dart @@ -13,7 +13,7 @@ StatefulWidget getSubscriptionPage({bool isOnBoarding = false}) { _isUserCreatedPostStripeSupport()) { return StripeSubscriptionPage(isOnboarding: isOnBoarding); } else { - return SubscriptionPage(isOnboarding: isOnBoarding); + return StripeSubscriptionPage(isOnboarding: isOnBoarding); } } diff --git a/lib/ui/payment/subscription_common_widgets.dart b/lib/ui/payment/subscription_common_widgets.dart index 301867c4e..5f0244150 100644 --- a/lib/ui/payment/subscription_common_widgets.dart +++ b/lib/ui/payment/subscription_common_widgets.dart @@ -2,15 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:photos/models/subscription.dart'; import 'package:photos/ui/billing_questions_widget.dart'; -import 'package:photos/ui/loading_widget.dart'; import 'package:photos/utils/data_util.dart'; import 'package:photos/utils/date_time_util.dart'; class SubscriptionHeaderWidget extends StatefulWidget { final bool isOnboarding; - final Future usageFuture; + final int currentUsage; - const SubscriptionHeaderWidget({Key key, this.isOnboarding, this.usageFuture}) + const SubscriptionHeaderWidget( + {Key key, this.isOnboarding, this.currentUsage}) : super(key: key); @override @@ -36,23 +36,9 @@ class _SubscriptionHeaderWidgetState extends State { } else { return SizedBox( height: 50, - child: FutureBuilder( - future: widget.usageFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Text("current usage is " + formatBytes(snapshot.data)), - ); - } else if (snapshot.hasError) { - return Container(); - } else { - return Padding( - padding: const EdgeInsets.all(16.0), - child: loadWidget, - ); - } - }, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text("current usage is " + formatBytes(widget.currentUsage)), ), ); } diff --git a/lib/ui/payment/subscription_page.dart b/lib/ui/payment/subscription_page.dart index 730c49d7a..b48c67c1d 100644 --- a/lib/ui/payment/subscription_page.dart +++ b/lib/ui/payment/subscription_page.dart @@ -11,6 +11,7 @@ import 'package:photos/events/subscription_purchased_event.dart'; import 'package:photos/models/billing_plan.dart'; import 'package:photos/models/subscription.dart'; import 'package:photos/services/billing_service.dart'; +import 'package:photos/services/user_service.dart'; import 'package:photos/ui/loading_widget.dart'; import 'package:photos/ui/payment/skip_subscription_widget.dart'; import 'package:photos/ui/payment/subscription_common_widgets.dart'; @@ -29,16 +30,17 @@ class SubscriptionPage extends StatefulWidget { }) : super(key: key); @override - State createState() => _SubscriptionPageState(); + State createState() => _SubscriptionPageState(); } class _SubscriptionPageState extends State { final _logger = Logger("SubscriptionPage"); final _billingService = BillingService.instance; + final _userService = UserService.instance; Subscription _currentSubscription; StreamSubscription _purchaseUpdateSubscription; ProgressDialog _dialog; - Future _usageFuture; + int _usage; bool _hasActiveSubscription; FreePlan _freePlan; List _plans; @@ -48,8 +50,8 @@ class _SubscriptionPageState extends State { @override void initState() { _billingService.setIsOnSubscriptionPage(true); - _billingService.fetchSubscription().then((subscription) async { - _currentSubscription = subscription; + _userService.getUserDetailsV2(memberCount: false).then((userDetails) async { + _currentSubscription = userDetails.subscription; _hasActiveSubscription = _currentSubscription.isValid(); final billingPlans = await _billingService.getBillingPlans(); _isActiveStripeSubscriber = @@ -59,12 +61,12 @@ class _SubscriptionPageState extends State { final productID = _isActiveStripeSubscriber ? plan.stripeID : Platform.isAndroid - ? plan.androidID - : plan.iosID; + ? plan.androidID + : plan.iosID; return productID != null && productID.isNotEmpty; }).toList(); _freePlan = billingPlans.freePlan; - _usageFuture = _billingService.fetchUsage(); + _usage = userDetails.usage; _hasLoadedData = true; setState(() {}); }); @@ -160,7 +162,7 @@ class _SubscriptionPageState extends State { final widgets = []; widgets.add(SubscriptionHeaderWidget( isOnboarding: widget.isOnboarding, - usageFuture: _usageFuture, + currentUsage: _usage, )); widgets.addAll([ @@ -177,7 +179,7 @@ class _SubscriptionPageState extends State { widgets.add(ValidityWidget(currentSubscription: _currentSubscription)); } - if ( _currentSubscription.productID == kFreeProductID) { + if (_currentSubscription.productID == kFreeProductID) { if (widget.isOnboarding) { widgets.add(SkipSubscriptionWidget(freePlan: _freePlan)); } @@ -306,18 +308,17 @@ class _SubscriptionPageState extends State { return; } await _dialog.show(); - if (_usageFuture != null) { - final usage = await _usageFuture; - if (usage > plan.storage) { - await _dialog.hide(); - showErrorDialog( - context, "sorry", "you cannot downgrade to this plan"); - return; - } + + if (_usage > plan.storage) { + await _dialog.hide(); + showErrorDialog( + context, "sorry", "you cannot downgrade to this plan"); + return; } + final ProductDetailsResponse response = - await InAppPurchaseConnection.instance - .queryProductDetails({productID}); + await InAppPurchaseConnection.instance + .queryProductDetails({productID}); if (response.notFoundIDs.isNotEmpty) { _logger.severe("Could not find products: " + response.notFoundIDs.toString()); @@ -331,8 +332,8 @@ class _SubscriptionPageState extends State { _currentSubscription.productID != plan.androidID; if (isCrossGradingOnAndroid) { final existingProductDetailsResponse = - await InAppPurchaseConnection.instance - .queryProductDetails({_currentSubscription.productID}); + await InAppPurchaseConnection.instance + .queryProductDetails({_currentSubscription.productID}); if (existingProductDetailsResponse.notFoundIDs.isNotEmpty) { _logger.severe("Could not find existing products: " + response.notFoundIDs.toString()); @@ -401,4 +402,3 @@ class _SubscriptionPageState extends State { ); } } - From 54fa6aacc1cc0b7967d39fd59db45c5805ce01e9 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 16 Apr 2022 22:10:18 +0530 Subject: [PATCH 03/14] Add option to manage family on sub page --- lib/core/constants.dart | 2 + lib/ui/payment/stripe_subscription_page.dart | 58 +++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 1579c0ec3..135e6132b 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -27,3 +27,5 @@ const kMnemonicKeyWordCount = 24; // https://stackoverflow.com/a/61162219 const kDragSensitivity = 8; + +const kFamilyPlanManagementUrl = "https://family.ente.io"; diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index 1e46766b4..dff86a95e 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:logging/logging.dart'; +import 'package:photos/core/constants.dart'; import 'package:photos/models/billing_plan.dart'; import 'package:photos/models/subscription.dart'; import 'package:photos/services/billing_service.dart'; @@ -183,7 +185,7 @@ class _StripeSubscriptionPageState extends State { } }, child: Container( - padding: EdgeInsets.fromLTRB(40, 80, 40, 80), + padding: EdgeInsets.fromLTRB(40, 80, 40, 40), child: Column( children: [ RichText( @@ -205,6 +207,41 @@ class _StripeSubscriptionPageState extends State { ), ), ]); + + widgets.addAll([ + Align( + alignment: Alignment.topCenter, + child: GestureDetector( + onTap: () async { + if (Platform.isAndroid) { + _launchFamilyPortal(); + } else { + showToast('visit web.ente.io to manage family'); + } + }, + child: Container( + padding: EdgeInsets.fromLTRB(40, 0, 40, 80), + child: Column( + children: [ + RichText( + text: TextSpan( + text: Platform.isAndroid + ? "manage family" + : "visit website to manage family plan", + style: TextStyle( + color: Platform.isAndroid ? Colors.blue : Colors.white, + fontFamily: 'Ubuntu', + fontSize: 15, + ), + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ]); } return SingleChildScrollView( @@ -233,6 +270,25 @@ class _StripeSubscriptionPageState extends State { await _dialog.hide(); } + Future _launchFamilyPortal() async { + await _dialog.show(); + try { + String jwtToken = await _userService.getPaymentToken(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return WebPage( + "family", '$kFamilyPlanManagementUrl?token=$jwtToken'); + }, + ), + ).then((value) => onWebPaymentGoBack); + } catch (e) { + await _dialog.hide(); + showGenericErrorDialog(context); + } + await _dialog.hide(); + } + Widget _stripeRenewOrCancelButton() { bool isRenewCancelled = _currentSubscription.attributes?.isCancelled ?? false; From 6e183545bcfbf0f01cd3542aa78ead1f34323d2c Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 20 Apr 2022 15:45:08 +0530 Subject: [PATCH 04/14] Support for fetching families jwt token Signed-off-by: Neeraj Gupta <254676+ua741@users.noreply.github.com> --- lib/services/user_service.dart | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 9ab4e89dc..33fe14a10 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -690,6 +690,27 @@ class UserService { } } + Future getFamiliesToken() async { + try { + var response = await _dio.get( + _config.getHttpEndpoint() + "/users/families-token", + options: Options( + headers: { + "X-Auth-Token": _config.getToken(), + }, + ), + ); + if (response != null && response.statusCode == 200) { + return response.data["familiesToken"]; + } else { + throw Exception("non 200 ok response"); + } + } catch (e, s) { + _logger.severe("failed to fetch families token", e, s); + rethrow; + } + } + Future _saveConfiguration(Response response) async { await Configuration.instance.setUserID(response.data["id"]); if (response.data["encryptedToken"] != null) { From b2df82bb2e926c6161fc05072bded7fb1a9cb07b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 21 Apr 2022 01:06:33 +0530 Subject: [PATCH 05/14] Add ChildSubscription Widget with support to leave family --- lib/services/user_service.dart | 14 ++++ lib/ui/payment/child_subscription_widget.dart | 74 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 lib/ui/payment/child_subscription_widget.dart diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 33fe14a10..6f638dc95 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -173,6 +173,20 @@ class UserService { } } + Future leaveFamilyPlan() async { + try { + await _dio.delete(_config.getHttpEndpoint() + "/family/leave", + options: Options( + headers: { + "X-Auth-Token": _config.getToken(), + }, + )); + } on DioError catch (e) { + _logger.warning('failed to leave family plan', e); + rethrow; + } + } + Future logout(BuildContext context) async { final dialog = createProgressDialog(context, "logging out..."); await dialog.show(); diff --git a/lib/ui/payment/child_subscription_widget.dart b/lib/ui/payment/child_subscription_widget.dart new file mode 100644 index 000000000..0590ad62c --- /dev/null +++ b/lib/ui/payment/child_subscription_widget.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:photos/models/user_details.dart'; +import 'package:photos/services/user_service.dart'; +import 'package:photos/ui/common/dialogs.dart'; +import 'package:photos/ui/common_elements.dart'; +import 'package:photos/utils/dialog_util.dart'; + +class ChildSubscriptionWidget extends StatelessWidget { + const ChildSubscriptionWidget({ + Key key, + @required this.userDetails, + }) : super(key: key); + + final UserDetails userDetails; + + @override + Widget build(BuildContext context) { + final String familyAdmin = userDetails.familyData.members + .firstWhere((element) => element.isAdmin) + .email; + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), + child: Row( + children: [ + Expanded( + child: Text( + "only your family plan admin ($familyAdmin) can change the plan.", + style: TextStyle( + fontSize: 16, + height: 1.3, + color: Colors.white, + ), + ), + ), + ], + ), + ), + button( + "leave family", + onPressed: () async { + await _leaveFamilyPlan(context); + }, + fontSize: 18, + ), + ], + ); + } + + Future _leaveFamilyPlan(BuildContext context) async { + final choice = await showChoiceDialog( + context, + 'leave family', + 'are you sure that you want to leave the family plan?', + firstAction: 'no', + secondAction: 'yes', + firstActionColor: Theme.of(context).buttonColor, + secondActionColor: Colors.white, + ); + if (choice != DialogUserChoice.secondChoice) { + return; + } + final dialog = createProgressDialog(context, "please wait..."); + await dialog.show(); + try { + await UserService.instance.leaveFamilyPlan(); + dialog.hide(); + } catch (e) { + dialog.hide(); + showGenericErrorDialog(context); + } + } +} From 4bcde8d308338feea8a59c3055facc81e124b81e Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 21 Apr 2022 01:44:44 +0530 Subject: [PATCH 06/14] msupport for managing family --- lib/core/constants.dart | 2 -- lib/models/user_details.dart | 11 +++++++ lib/services/billing_service.dart | 4 +++ lib/ui/payment/stripe_subscription_page.dart | 31 ++++++++++---------- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/lib/core/constants.dart b/lib/core/constants.dart index 135e6132b..1579c0ec3 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -27,5 +27,3 @@ const kMnemonicKeyWordCount = 24; // https://stackoverflow.com/a/61162219 const kDragSensitivity = 8; - -const kFamilyPlanManagementUrl = "https://family.ente.io"; diff --git a/lib/models/user_details.dart b/lib/models/user_details.dart index a1b237ad7..a1176665f 100644 --- a/lib/models/user_details.dart +++ b/lib/models/user_details.dart @@ -17,6 +17,17 @@ class UserDetails { this.familyData, ); + bool isPartOfFamily() { + return familyData?.members?.isNotEmpty ?? false; + } + + bool isFamilyAdmin() { + assert(isPartOfFamily(), "verify user is part of family before calling"); + final FamilyMember currentUserMember = familyData?.members + ?.firstWhere((element) => element.email.trim() == email.trim()); + return currentUserMember.isAdmin; + } + factory UserDetails.fromMap(Map map) { return UserDetails( map['email'] as String, diff --git a/lib/services/billing_service.dart b/lib/services/billing_service.dart index 5b45b13e1..ff114e5c8 100644 --- a/lib/services/billing_service.dart +++ b/lib/services/billing_service.dart @@ -14,6 +14,10 @@ const kWebPaymentRedirectUrl = "https://payments.ente.io/frameRedirect"; const kWebPaymentBaseEndpoint = String.fromEnvironment("web-payment", defaultValue: "https://payments.ente.io"); +const kFamilyPlanManagementUrl = String.fromEnvironment("web-family", + defaultValue: "https://family.ente.io"); +// "http://localhost:3003"; + class BillingService { BillingService._privateConstructor(); diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index dff86a95e..cb4e60d3d 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -1,17 +1,16 @@ import 'dart:async'; -import 'dart:io'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:logging/logging.dart'; -import 'package:photos/core/constants.dart'; import 'package:photos/models/billing_plan.dart'; import 'package:photos/models/subscription.dart'; +import 'package:photos/models/user_details.dart'; import 'package:photos/services/billing_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/ui/common/dialogs.dart'; import 'package:photos/ui/loading_widget.dart'; +import 'package:photos/ui/payment/child_subscription_widget.dart'; import 'package:photos/ui/payment/payment_web_page.dart'; import 'package:photos/ui/payment/skip_subscription_widget.dart'; import 'package:photos/ui/payment/subscription_common_widgets.dart'; @@ -40,6 +39,7 @@ class _StripeSubscriptionPageState extends State { final _userService = UserService.instance; Subscription _currentSubscription; ProgressDialog _dialog; + UserDetails _userDetails; int _usage; // indicates if user's subscription plan is still active @@ -59,6 +59,7 @@ class _StripeSubscriptionPageState extends State { Future _fetchSub() async { return _userService.getUserDetailsV2().then((userDetails) async { + _userDetails = userDetails; _currentSubscription = userDetails.subscription; _showYearlyPlan = _currentSubscription.isYearlyPlan(); _hasActiveSubscription = _currentSubscription.isValid(); @@ -122,6 +123,9 @@ class _StripeSubscriptionPageState extends State { Widget _getBody() { if (_hasLoadedData) { + if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) { + return ChildSubscriptionWidget(userDetails: _userDetails); + } return _buildPlans(); } return loadWidget; @@ -185,7 +189,7 @@ class _StripeSubscriptionPageState extends State { } }, child: Container( - padding: EdgeInsets.fromLTRB(40, 80, 40, 40), + padding: EdgeInsets.fromLTRB(40, 80, 40, 20), child: Column( children: [ RichText( @@ -213,11 +217,7 @@ class _StripeSubscriptionPageState extends State { alignment: Alignment.topCenter, child: GestureDetector( onTap: () async { - if (Platform.isAndroid) { - _launchFamilyPortal(); - } else { - showToast('visit web.ente.io to manage family'); - } + _launchFamilyPortal(); }, child: Container( padding: EdgeInsets.fromLTRB(40, 0, 40, 80), @@ -225,11 +225,9 @@ class _StripeSubscriptionPageState extends State { children: [ RichText( text: TextSpan( - text: Platform.isAndroid - ? "manage family" - : "visit website to manage family plan", + text: "manage family", style: TextStyle( - color: Platform.isAndroid ? Colors.blue : Colors.white, + color: Colors.blue, fontFamily: 'Ubuntu', fontSize: 15, ), @@ -273,12 +271,13 @@ class _StripeSubscriptionPageState extends State { Future _launchFamilyPortal() async { await _dialog.show(); try { - String jwtToken = await _userService.getPaymentToken(); + final String jwtToken = await _userService.getFamiliesToken(); + final bool familyExist = _userDetails.isPartOfFamily(); Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { - return WebPage( - "family", '$kFamilyPlanManagementUrl?token=$jwtToken'); + return WebPage("family", + '$kFamilyPlanManagementUrl?token=$jwtToken&familyCreated=$familyExist'); }, ), ).then((value) => onWebPaymentGoBack); From 151456953d7bf4706b79e193dabe14eb3e43839a Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 21 Apr 2022 02:06:35 +0530 Subject: [PATCH 07/14] use totalUsage while checking for downgrade --- lib/models/user_details.dart | 16 ++++++++++++++++ lib/ui/payment/stripe_subscription_page.dart | 7 ++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/models/user_details.dart b/lib/models/user_details.dart index a1176665f..aa5bdc0ac 100644 --- a/lib/models/user_details.dart +++ b/lib/models/user_details.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:photos/models/subscription.dart'; class UserDetails { @@ -28,6 +29,17 @@ class UserDetails { return currentUserMember.isAdmin; } + // getFamilyOrPersonalUsage will return total usage for family if user + // belong to family group. Otherwise, it will return storage consumed by + // current user + int getFamilyOrPersonalUsage() { + return isPartOfFamily() ? familyData.getTotalUsage() : usage; + } + + int getPersonalUsage() { + return usage; + } + factory UserDetails.fromMap(Map map) { return UserDetails( map['email'] as String, @@ -82,6 +94,10 @@ class FamilyData { FamilyData(this.members, this.storage, this.expiryTime); + int getTotalUsage() { + return members.map((e) => e.usage).toList().sum; + } + factory FamilyData.fromMap(Map map) { if (map == null) { return null; diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index cb4e60d3d..6be6121b3 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -40,7 +40,6 @@ class _StripeSubscriptionPageState extends State { Subscription _currentSubscription; ProgressDialog _dialog; UserDetails _userDetails; - int _usage; // indicates if user's subscription plan is still active bool _hasActiveSubscription; @@ -64,7 +63,6 @@ class _StripeSubscriptionPageState extends State { _showYearlyPlan = _currentSubscription.isYearlyPlan(); _hasActiveSubscription = _currentSubscription.isValid(); _isStripeSubscriber = _currentSubscription.paymentProvider == kStripe; - _usage = userDetails.usage; return _filterStripeForUI().then((value) { _hasLoadedData = true; setState(() {}); @@ -136,7 +134,7 @@ class _StripeSubscriptionPageState extends State { widgets.add(SubscriptionHeaderWidget( isOnboarding: widget.isOnboarding, - currentUsage: _usage, + currentUsage: _userDetails.getPersonalUsage(), )); widgets.addAll([ @@ -365,12 +363,11 @@ class _StripeSubscriptionPageState extends State { "please cancel your existing subscription from ${_currentSubscription.paymentProvider} first"); return; } - if (_usage > plan.storage) { + if (_userDetails.getFamilyOrPersonalUsage() > plan.storage) { showErrorDialog( context, "sorry", "you cannot downgrade to this plan"); return; } - await _dialog.show(); String stripPurChaseAction = 'buy'; if (_isStripeSubscriber && _hasActiveSubscription) { // confirm if user wants to change plan or not From 830d7bed7aef943db6d3e79d8eb4627642af1219 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 21 Apr 2022 02:42:34 +0530 Subject: [PATCH 08/14] support family plan for non-stripe customers --- lib/ui/payment/stripe_subscription_page.dart | 53 ++++++++------- lib/ui/payment/subscription.dart | 2 +- .../payment/subscription_common_widgets.dart | 3 +- lib/ui/payment/subscription_page.dart | 68 ++++++++++++++++--- 4 files changed, 90 insertions(+), 36 deletions(-) diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index 6be6121b3..9d0db6089 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -123,8 +123,9 @@ class _StripeSubscriptionPageState extends State { if (_hasLoadedData) { if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) { return ChildSubscriptionWidget(userDetails: _userDetails); + } else { + return _buildPlans(); } - return _buildPlans(); } return loadWidget; } @@ -210,34 +211,36 @@ class _StripeSubscriptionPageState extends State { ), ]); - widgets.addAll([ - Align( - alignment: Alignment.topCenter, - child: GestureDetector( - onTap: () async { - _launchFamilyPortal(); - }, - child: Container( - padding: EdgeInsets.fromLTRB(40, 0, 40, 80), - child: Column( - children: [ - RichText( - text: TextSpan( - text: "manage family", - style: TextStyle( - color: Colors.blue, - fontFamily: 'Ubuntu', - fontSize: 15, + if (!widget.isOnboarding) { + widgets.addAll([ + Align( + alignment: Alignment.topCenter, + child: GestureDetector( + onTap: () async { + await _launchFamilyPortal(); + }, + child: Container( + padding: EdgeInsets.fromLTRB(40, 0, 40, 80), + child: Column( + children: [ + RichText( + text: TextSpan( + text: "manage family", + style: TextStyle( + color: Colors.blue, + fontFamily: 'Ubuntu', + fontSize: 15, + ), ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), - ], + ], + ), ), ), ), - ), - ]); + ]); + } } return SingleChildScrollView( @@ -278,7 +281,7 @@ class _StripeSubscriptionPageState extends State { '$kFamilyPlanManagementUrl?token=$jwtToken&familyCreated=$familyExist'); }, ), - ).then((value) => onWebPaymentGoBack); + ); } catch (e) { await _dialog.hide(); showGenericErrorDialog(context); diff --git a/lib/ui/payment/subscription.dart b/lib/ui/payment/subscription.dart index 7bef46e59..7beb8a66a 100644 --- a/lib/ui/payment/subscription.dart +++ b/lib/ui/payment/subscription.dart @@ -13,7 +13,7 @@ StatefulWidget getSubscriptionPage({bool isOnBoarding = false}) { _isUserCreatedPostStripeSupport()) { return StripeSubscriptionPage(isOnboarding: isOnBoarding); } else { - return StripeSubscriptionPage(isOnboarding: isOnBoarding); + return SubscriptionPage(isOnboarding: isOnBoarding); } } diff --git a/lib/ui/payment/subscription_common_widgets.dart b/lib/ui/payment/subscription_common_widgets.dart index 5f0244150..e79c21ca1 100644 --- a/lib/ui/payment/subscription_common_widgets.dart +++ b/lib/ui/payment/subscription_common_widgets.dart @@ -8,9 +8,10 @@ import 'package:photos/utils/date_time_util.dart'; class SubscriptionHeaderWidget extends StatefulWidget { final bool isOnboarding; final int currentUsage; + final int familyUsage; const SubscriptionHeaderWidget( - {Key key, this.isOnboarding, this.currentUsage}) + {Key key, this.isOnboarding, this.currentUsage, this.familyUsage}) : super(key: key); @override diff --git a/lib/ui/payment/subscription_page.dart b/lib/ui/payment/subscription_page.dart index b48c67c1d..d4ee060c8 100644 --- a/lib/ui/payment/subscription_page.dart +++ b/lib/ui/payment/subscription_page.dart @@ -10,13 +10,16 @@ import 'package:photos/core/event_bus.dart'; import 'package:photos/events/subscription_purchased_event.dart'; import 'package:photos/models/billing_plan.dart'; import 'package:photos/models/subscription.dart'; +import 'package:photos/models/user_details.dart'; import 'package:photos/services/billing_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/ui/loading_widget.dart'; +import 'package:photos/ui/payment/child_subscription_widget.dart'; import 'package:photos/ui/payment/skip_subscription_widget.dart'; import 'package:photos/ui/payment/subscription_common_widgets.dart'; import 'package:photos/ui/payment/subscription_plan_widget.dart'; import 'package:photos/ui/progress_dialog.dart'; +import 'package:photos/ui/web_page.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/toast_util.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -40,7 +43,7 @@ class _SubscriptionPageState extends State { Subscription _currentSubscription; StreamSubscription _purchaseUpdateSubscription; ProgressDialog _dialog; - int _usage; + UserDetails _userDetails; bool _hasActiveSubscription; FreePlan _freePlan; List _plans; @@ -51,6 +54,7 @@ class _SubscriptionPageState extends State { void initState() { _billingService.setIsOnSubscriptionPage(true); _userService.getUserDetailsV2(memberCount: false).then((userDetails) async { + _userDetails = _userDetails; _currentSubscription = userDetails.subscription; _hasActiveSubscription = _currentSubscription.isValid(); final billingPlans = await _billingService.getBillingPlans(); @@ -66,7 +70,6 @@ class _SubscriptionPageState extends State { return productID != null && productID.isNotEmpty; }).toList(); _freePlan = billingPlans.freePlan; - _usage = userDetails.usage; _hasLoadedData = true; setState(() {}); }); @@ -153,7 +156,11 @@ class _SubscriptionPageState extends State { Widget _getBody() { if (_hasLoadedData) { - return _buildPlans(); + if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) { + return ChildSubscriptionWidget(userDetails: _userDetails); + } else { + return _buildPlans(); + } } return loadWidget; } @@ -162,7 +169,7 @@ class _SubscriptionPageState extends State { final widgets = []; widgets.add(SubscriptionHeaderWidget( isOnboarding: widget.isOnboarding, - currentUsage: _usage, + currentUsage: _userDetails.getPersonalUsage(), )); widgets.addAll([ @@ -230,6 +237,34 @@ class _SubscriptionPageState extends State { ), ), ]); + widgets.addAll([ + Align( + alignment: Alignment.topCenter, + child: GestureDetector( + onTap: () async { + _launchFamilyPortal(); + }, + child: Container( + padding: EdgeInsets.fromLTRB(40, 0, 40, 80), + child: Column( + children: [ + RichText( + text: TextSpan( + text: "manage family", + style: TextStyle( + color: Colors.blue, + fontFamily: 'Ubuntu', + fontSize: 15, + ), + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + ]); } return SingleChildScrollView( child: Column( @@ -307,15 +342,12 @@ class _SubscriptionPageState extends State { if (isActive) { return; } - await _dialog.show(); - - if (_usage > plan.storage) { - await _dialog.hide(); + if (_userDetails.getFamilyOrPersonalUsage() > plan.storage) { showErrorDialog( context, "sorry", "you cannot downgrade to this plan"); return; } - + await _dialog.show(); final ProductDetailsResponse response = await InAppPurchaseConnection.instance .queryProductDetails({productID}); @@ -401,4 +433,22 @@ class _SubscriptionPageState extends State { ), ); } + + Future _launchStripePortal() async { + await _dialog.show(); + try { + String url = await _billingService.getStripeCustomerPortalUrl(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return WebPage("payment details", url); + }, + ), + ).then((value) => onWebPaymentGoBack); + } catch (e) { + await _dialog.hide(); + showGenericErrorDialog(context); + } + await _dialog.hide(); + } } From f4e4829669dd7cc470e17fda0d39ff067433035c Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 21 Apr 2022 08:48:44 +0530 Subject: [PATCH 09/14] Show manage family option for app store & play store --- lib/ui/payment/stripe_subscription_page.dart | 14 ++++++------- lib/ui/payment/subscription_page.dart | 22 ++++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index 9d0db6089..9104eaa60 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -274,14 +274,12 @@ class _StripeSubscriptionPageState extends State { try { final String jwtToken = await _userService.getFamiliesToken(); final bool familyExist = _userDetails.isPartOfFamily(); - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage("family", - '$kFamilyPlanManagementUrl?token=$jwtToken&familyCreated=$familyExist'); - }, - ), - ); + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) { + return WebPage("family", + '$kFamilyPlanManagementUrl?token=$jwtToken&familyCreated=$familyExist'); + }, + )).then((value) => onWebPaymentGoBack); } catch (e) { await _dialog.hide(); showGenericErrorDialog(context); diff --git a/lib/ui/payment/subscription_page.dart b/lib/ui/payment/subscription_page.dart index d4ee060c8..6bf0792d8 100644 --- a/lib/ui/payment/subscription_page.dart +++ b/lib/ui/payment/subscription_page.dart @@ -54,7 +54,7 @@ class _SubscriptionPageState extends State { void initState() { _billingService.setIsOnSubscriptionPage(true); _userService.getUserDetailsV2(memberCount: false).then((userDetails) async { - _userDetails = _userDetails; + _userDetails = userDetails; _currentSubscription = userDetails.subscription; _hasActiveSubscription = _currentSubscription.isValid(); final billingPlans = await _billingService.getBillingPlans(); @@ -213,7 +213,7 @@ class _SubscriptionPageState extends State { } }, child: Container( - padding: EdgeInsets.fromLTRB(40, 80, 40, 80), + padding: EdgeInsets.fromLTRB(40, 80, 40, 20), child: Column( children: [ RichText( @@ -434,17 +434,17 @@ class _SubscriptionPageState extends State { ); } - Future _launchStripePortal() async { + Future _launchFamilyPortal() async { await _dialog.show(); try { - String url = await _billingService.getStripeCustomerPortalUrl(); - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage("payment details", url); - }, - ), - ).then((value) => onWebPaymentGoBack); + final String jwtToken = await _userService.getFamiliesToken(); + final bool familyExist = _userDetails.isPartOfFamily(); + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) { + return WebPage("family", + '$kFamilyPlanManagementUrl?token=$jwtToken&familyCreated=$familyExist'); + }, + )); } catch (e) { await _dialog.hide(); showGenericErrorDialog(context); From d2d99758bec06c8dd35f8f0012c1b08cf0140fdd Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 21 Apr 2022 08:51:58 +0530 Subject: [PATCH 10/14] Show family usage as current usage on subscription page --- lib/ui/payment/stripe_subscription_page.dart | 2 +- lib/ui/payment/subscription_common_widgets.dart | 3 +-- lib/ui/payment/subscription_page.dart | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index 9104eaa60..81bfe034a 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -135,7 +135,7 @@ class _StripeSubscriptionPageState extends State { widgets.add(SubscriptionHeaderWidget( isOnboarding: widget.isOnboarding, - currentUsage: _userDetails.getPersonalUsage(), + currentUsage: _userDetails.getFamilyOrPersonalUsage(), )); widgets.addAll([ diff --git a/lib/ui/payment/subscription_common_widgets.dart b/lib/ui/payment/subscription_common_widgets.dart index e79c21ca1..5f0244150 100644 --- a/lib/ui/payment/subscription_common_widgets.dart +++ b/lib/ui/payment/subscription_common_widgets.dart @@ -8,10 +8,9 @@ import 'package:photos/utils/date_time_util.dart'; class SubscriptionHeaderWidget extends StatefulWidget { final bool isOnboarding; final int currentUsage; - final int familyUsage; const SubscriptionHeaderWidget( - {Key key, this.isOnboarding, this.currentUsage, this.familyUsage}) + {Key key, this.isOnboarding, this.currentUsage}) : super(key: key); @override diff --git a/lib/ui/payment/subscription_page.dart b/lib/ui/payment/subscription_page.dart index 6bf0792d8..6e7d5f396 100644 --- a/lib/ui/payment/subscription_page.dart +++ b/lib/ui/payment/subscription_page.dart @@ -169,7 +169,7 @@ class _SubscriptionPageState extends State { final widgets = []; widgets.add(SubscriptionHeaderWidget( isOnboarding: widget.isOnboarding, - currentUsage: _userDetails.getPersonalUsage(), + currentUsage: _userDetails.getFamilyOrPersonalUsage(), )); widgets.addAll([ From 8324fd5def3dc81a9e77faa098bf572c3a1a212f Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 21 Apr 2022 09:52:58 +0530 Subject: [PATCH 11/14] Update usage detail section for family plan --- lib/models/user_details.dart | 10 ++++++ lib/ui/settings/details_section_widget.dart | 39 +++++++++++++-------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/lib/models/user_details.dart b/lib/models/user_details.dart index aa5bdc0ac..de1ba3a82 100644 --- a/lib/models/user_details.dart +++ b/lib/models/user_details.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:collection/collection.dart'; import 'package:photos/models/subscription.dart'; @@ -36,6 +38,14 @@ class UserDetails { return isPartOfFamily() ? familyData.getTotalUsage() : usage; } + int getFreeStorage() { + return max( + isPartOfFamily() + ? (familyData.storage - familyData.getTotalUsage()) + : (subscription.storage - usage), + 0); + } + int getPersonalUsage() { return usage; } diff --git a/lib/ui/settings/details_section_widget.dart b/lib/ui/settings/details_section_widget.dart index 38ba20355..758db5c39 100644 --- a/lib/ui/settings/details_section_widget.dart +++ b/lib/ui/settings/details_section_widget.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -27,13 +26,14 @@ class _DetailsSectionWidgetState extends State { void initState() { super.initState(); _fetchUserDetails(); - _userDetailsChangedEvent = Bus.instance.on().listen((event) { + _userDetailsChangedEvent = + Bus.instance.on().listen((event) { _fetchUserDetails(); }); } void _fetchUserDetails() { - UserService.instance.getUserDetails().then((details) { + UserService.instance.getUserDetailsV2(memberCount: true).then((details) { setState(() { _userDetails = details; }); @@ -69,23 +69,31 @@ class _DetailsSectionWidgetState extends State { children: [ GestureDetector( onTap: () { - showToast(formatBytes( - _userDetails.subscription.storage - _userDetails.usage) + + int totalStorage = _userDetails.isPartOfFamily() + ? _userDetails.familyData.storage + : _userDetails.subscription.storage; + String usageText = formatBytes(_userDetails.getFreeStorage()) + " / " + - convertBytesToReadableFormat( - _userDetails.subscription.storage) + - " free"); + convertBytesToReadableFormat(totalStorage) + + " free"; + if (_userDetails.isPartOfFamily()) { + usageText += + "\npersonal usage: ${convertBytesToReadableFormat(_userDetails.getPersonalUsage())}\n" + "family usage: ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage() - _userDetails.getPersonalUsage())}"; + } + showToast(usageText); }, child: PieChart( dataMap: { - "used": _userDetails.usage.toDouble(), - "free": max( - _userDetails.subscription.storage.toDouble() - - _userDetails.usage.toDouble(), - 0), + "used": _userDetails.getPersonalUsage().toDouble(), + "family_usage": (_userDetails.getFamilyOrPersonalUsage() - + _userDetails.getPersonalUsage()) + .toDouble(), + "free": _userDetails.getFreeStorage().toDouble(), }, colorList: const [ Colors.redAccent, + Colors.blueGrey, Color.fromRGBO(50, 194, 100, 1.0), ], legendOptions: LegendOptions( @@ -98,8 +106,9 @@ class _DetailsSectionWidgetState extends State { chartRadius: 80, ringStrokeWidth: 4, chartType: ChartType.ring, - centerText: - convertBytesToReadableFormat(_userDetails.usage) + "\nused", + centerText: convertBytesToReadableFormat( + _userDetails.getPersonalUsage()) + + "\nused", centerTextStyle: TextStyle( color: Colors.white, fontSize: 12, From 4151b18b430d14eb10603239b8c43dae0ab61d59 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 21 Apr 2022 10:04:15 +0530 Subject: [PATCH 12/14] remove redundant comment --- lib/services/billing_service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/services/billing_service.dart b/lib/services/billing_service.dart index ff114e5c8..7e5f24b9d 100644 --- a/lib/services/billing_service.dart +++ b/lib/services/billing_service.dart @@ -16,7 +16,6 @@ const kWebPaymentBaseEndpoint = String.fromEnvironment("web-payment", const kFamilyPlanManagementUrl = String.fromEnvironment("web-family", defaultValue: "https://family.ente.io"); -// "http://localhost:3003"; class BillingService { BillingService._privateConstructor(); From 896ac3cf29b445b5eefee91c602fd3ca6e0b350d Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 21 Apr 2022 20:25:37 +0530 Subject: [PATCH 13/14] update url for family portal --- lib/services/billing_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/billing_service.dart b/lib/services/billing_service.dart index 7e5f24b9d..59cd9b0c5 100644 --- a/lib/services/billing_service.dart +++ b/lib/services/billing_service.dart @@ -15,7 +15,7 @@ const kWebPaymentBaseEndpoint = String.fromEnvironment("web-payment", defaultValue: "https://payments.ente.io"); const kFamilyPlanManagementUrl = String.fromEnvironment("web-family", - defaultValue: "https://family.ente.io"); + defaultValue: "https://families.ente.io"); class BillingService { BillingService._privateConstructor(); From cb342cbb2d25bccadb07545d4895539c860a9a17 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 25 Apr 2022 09:51:43 +0530 Subject: [PATCH 14/14] improve UI for leave family --- assets/family_sharing.jpg | Bin 0 -> 48242 bytes lib/ui/payment/child_subscription_widget.dart | 120 ++++++++++++++---- 2 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 assets/family_sharing.jpg diff --git a/assets/family_sharing.jpg b/assets/family_sharing.jpg new file mode 100644 index 0000000000000000000000000000000000000000..20adb7ba5d9a8d141454cd7227be54baf7063970 GIT binary patch literal 48242 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 literal 0 HcmV?d00001 diff --git a/lib/ui/payment/child_subscription_widget.dart b/lib/ui/payment/child_subscription_widget.dart index 0590ad62c..2e60c7627 100644 --- a/lib/ui/payment/child_subscription_widget.dart +++ b/lib/ui/payment/child_subscription_widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter/painting.dart'; import 'package:photos/models/user_details.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/ui/common/dialogs.dart'; -import 'package:photos/ui/common_elements.dart'; import 'package:photos/utils/dialog_util.dart'; class ChildSubscriptionWidget extends StatelessWidget { @@ -18,33 +18,107 @@ class ChildSubscriptionWidget extends StatelessWidget { final String familyAdmin = userDetails.familyData.members .firstWhere((element) => element.isAdmin) .email; - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), - child: Row( - children: [ - Expanded( - child: Text( - "only your family plan admin ($familyAdmin) can change the plan.", + return Padding( + padding: const EdgeInsets.symmetric(vertical: 32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: Text( + "you are on a family plan!", + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: "please contact ", + ), + TextSpan( + text: familyAdmin, + style: TextStyle(color: Color.fromRGBO(29, 185, 84, 1)), + ), + TextSpan( + text: " to manage your family subscription", + ), + ], + style: TextStyle( + fontFamily: 'Ubuntu', + fontSize: 14, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + ), + Image.asset( + "assets/family_sharing.jpg", + height: 256, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 0), + ), + InkWell( + child: OutlinedButton( + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + padding: EdgeInsets.symmetric(vertical: 18, horizontal: 100), + side: BorderSide( + width: 2, + color: Color.fromRGBO(255, 52, 52, 1), + ), + ), + child: Text( + "leave family", + style: TextStyle( + fontFamily: 'Ubuntu-Regular', + fontWeight: FontWeight.bold, + fontSize: 18, + color: Color.fromRGBO(255, 52, 52, 1), + ), + textAlign: TextAlign.center, + ), + onPressed: () async => {await _leaveFamilyPlan(context)}, + ), + ), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: const [ + TextSpan( + text: "please contact ", + ), + TextSpan( + text: "support@ente.io", + style: TextStyle(color: Color.fromRGBO(29, 185, 84, 1)), + ), + TextSpan( + text: " for help", + ), + ], style: TextStyle( - fontSize: 16, - height: 1.3, - color: Colors.white, + fontFamily: 'Ubuntu-Regular', + fontSize: 12, ), ), ), - ], + ), ), - ), - button( - "leave family", - onPressed: () async { - await _leaveFamilyPlan(context); - }, - fontSize: 18, - ), - ], + ], + ), ); }