Merge pull request #214 from ente-io/family_plan

Family plan
This commit is contained in:
Neeraj Gupta 2022-04-26 10:30:10 +05:30 committed by GitHub
commit 714e88b22d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 472 additions and 77 deletions

BIN
assets/family_sharing.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,3 +1,6 @@
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:photos/models/subscription.dart';
class UserDetails {
@ -6,6 +9,7 @@ class UserDetails {
final int fileCount;
final int sharedCollectionsCount;
final Subscription subscription;
final FamilyData familyData;
UserDetails(
this.email,
@ -13,8 +17,39 @@ class UserDetails {
this.fileCount,
this.sharedCollectionsCount,
this.subscription,
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;
}
// 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 getFreeStorage() {
return max(
isPartOfFamily()
? (familyData.storage - familyData.getTotalUsage())
: (subscription.storage - usage),
0);
}
int getPersonalUsage() {
return usage;
}
factory UserDetails.fromMap(Map<String, dynamic> map) {
return UserDetails(
map['email'] as String,
@ -22,6 +57,7 @@ class UserDetails {
map['fileCount'] as int,
map['sharedCollectionsCount'] as int,
Subscription.fromMap(map['subscription']),
FamilyData.fromMap(map['familyData']),
);
}
@ -32,6 +68,65 @@ 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<String, dynamic> map) {
return FamilyMember(
(map['email'] ?? '') as String,
map['usage'] as int,
map['id'] as String,
map['isAdmin'] as bool,
);
}
Map<String, dynamic> toMap() {
return {'email': email, 'usage': usage, 'id': id, 'isAdmin': isAdmin};
}
}
class FamilyData {
final List<FamilyMember> members;
// Storage available based on the family plan
final int storage;
final int expiryTime;
FamilyData(this.members, this.storage, this.expiryTime);
int getTotalUsage() {
return members.map((e) => e.usage).toList().sum;
}
factory FamilyData.fromMap(Map<String, dynamic> map) {
if (map == null) {
return null;
}
assert(map['members'] != null && map['members'].length >= 0);
final members = List<FamilyMember>.from(
map['members'].map((x) => FamilyMember.fromMap(x)));
return FamilyData(
members,
map['storage'] as int,
map['expiryTime'] as int,
);
}
Map<String, dynamic> toMap() {
return {
'members': members.map((x) => x?.toMap())?.toList(),
'storage': storage,
'expiryTime': expiryTime
};
}
}

View file

@ -14,6 +14,9 @@ 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://families.ente.io");
class BillingService {
BillingService._privateConstructor();

View file

@ -121,6 +121,24 @@ class UserService {
}
}
Future<UserDetails> 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<Sessions> getActiveSessions() async {
try {
final response = await _dio.get(
@ -155,6 +173,20 @@ class UserService {
}
}
Future<void> 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<void> logout(BuildContext context) async {
final dialog = createProgressDialog(context, "logging out...");
await dialog.show();
@ -672,6 +704,27 @@ class UserService {
}
}
Future<String> 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<void> _saveConfiguration(Response response) async {
await Configuration.instance.setUserID(response.data["id"]);
if (response.data["encryptedToken"] != null) {

View file

@ -0,0 +1,148 @@
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/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 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(
fontFamily: 'Ubuntu-Regular',
fontSize: 12,
),
),
),
),
),
],
),
);
}
Future<void> _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);
}
}
}

View file

@ -1,14 +1,16 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.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';
@ -34,9 +36,10 @@ class StripeSubscriptionPage extends StatefulWidget {
class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
final _logger = Logger("StripeSubscriptionPage");
final _billingService = BillingService.instance;
final _userService = UserService.instance;
Subscription _currentSubscription;
ProgressDialog _dialog;
Future<int> _usageFuture;
UserDetails _userDetails;
// indicates if user's subscription plan is still active
bool _hasActiveSubscription;
@ -54,12 +57,12 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
}
Future<void> _fetchSub() async {
return _billingService.fetchSubscription().then((subscription) async {
_currentSubscription = subscription;
return _userService.getUserDetailsV2().then((userDetails) async {
_userDetails = userDetails;
_currentSubscription = userDetails.subscription;
_showYearlyPlan = _currentSubscription.isYearlyPlan();
_hasActiveSubscription = _currentSubscription.isValid();
_isStripeSubscriber = _currentSubscription.paymentProvider == kStripe;
_usageFuture = _billingService.fetchUsage();
return _filterStripeForUI().then((value) {
_hasLoadedData = true;
setState(() {});
@ -118,7 +121,11 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
Widget _getBody() {
if (_hasLoadedData) {
return _buildPlans();
if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) {
return ChildSubscriptionWidget(userDetails: _userDetails);
} else {
return _buildPlans();
}
}
return loadWidget;
}
@ -128,7 +135,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
widgets.add(SubscriptionHeaderWidget(
isOnboarding: widget.isOnboarding,
usageFuture: _usageFuture,
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
));
widgets.addAll([
@ -181,7 +188,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
}
},
child: Container(
padding: EdgeInsets.fromLTRB(40, 80, 40, 80),
padding: EdgeInsets.fromLTRB(40, 80, 40, 20),
child: Column(
children: [
RichText(
@ -203,6 +210,37 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
),
),
]);
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,
),
],
),
),
),
),
]);
}
}
return SingleChildScrollView(
@ -231,6 +269,24 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
await _dialog.hide();
}
Future<void> _launchFamilyPortal() async {
await _dialog.show();
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');
},
)).then((value) => onWebPaymentGoBack);
} catch (e) {
await _dialog.hide();
showGenericErrorDialog(context);
}
await _dialog.hide();
}
Widget _stripeRenewOrCancelButton() {
bool isRenewCancelled =
_currentSubscription.attributes?.isCancelled ?? false;
@ -308,15 +364,10 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
"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 (_userDetails.getFamilyOrPersonalUsage() > plan.storage) {
showErrorDialog(
context, "sorry", "you cannot downgrade to this plan");
return;
}
String stripPurChaseAction = 'buy';
if (_isStripeSubscriber && _hasActiveSubscription) {

View file

@ -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<int> 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<SubscriptionHeaderWidget> {
} 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)),
),
);
}

View file

@ -10,12 +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';
@ -29,16 +33,17 @@ class SubscriptionPage extends StatefulWidget {
}) : super(key: key);
@override
State<SubscriptionPage> createState() => _SubscriptionPageState();
State<SubscriptionPage> createState() => _SubscriptionPageState();
}
class _SubscriptionPageState extends State<SubscriptionPage> {
final _logger = Logger("SubscriptionPage");
final _billingService = BillingService.instance;
final _userService = UserService.instance;
Subscription _currentSubscription;
StreamSubscription _purchaseUpdateSubscription;
ProgressDialog _dialog;
Future<int> _usageFuture;
UserDetails _userDetails;
bool _hasActiveSubscription;
FreePlan _freePlan;
List<BillingPlan> _plans;
@ -48,8 +53,9 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
@override
void initState() {
_billingService.setIsOnSubscriptionPage(true);
_billingService.fetchSubscription().then((subscription) async {
_currentSubscription = subscription;
_userService.getUserDetailsV2(memberCount: false).then((userDetails) async {
_userDetails = userDetails;
_currentSubscription = userDetails.subscription;
_hasActiveSubscription = _currentSubscription.isValid();
final billingPlans = await _billingService.getBillingPlans();
_isActiveStripeSubscriber =
@ -59,12 +65,11 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
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();
_hasLoadedData = true;
setState(() {});
});
@ -151,7 +156,11 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
Widget _getBody() {
if (_hasLoadedData) {
return _buildPlans();
if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) {
return ChildSubscriptionWidget(userDetails: _userDetails);
} else {
return _buildPlans();
}
}
return loadWidget;
}
@ -160,7 +169,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
final widgets = <Widget>[];
widgets.add(SubscriptionHeaderWidget(
isOnboarding: widget.isOnboarding,
usageFuture: _usageFuture,
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
));
widgets.addAll([
@ -177,7 +186,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
widgets.add(ValidityWidget(currentSubscription: _currentSubscription));
}
if ( _currentSubscription.productID == kFreeProductID) {
if (_currentSubscription.productID == kFreeProductID) {
if (widget.isOnboarding) {
widgets.add(SkipSubscriptionWidget(freePlan: _freePlan));
}
@ -204,7 +213,7 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
}
},
child: Container(
padding: EdgeInsets.fromLTRB(40, 80, 40, 80),
padding: EdgeInsets.fromLTRB(40, 80, 40, 20),
child: Column(
children: [
RichText(
@ -228,6 +237,34 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
),
),
]);
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(
@ -305,19 +342,15 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
if (isActive) {
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 (_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});
await InAppPurchaseConnection.instance
.queryProductDetails({productID});
if (response.notFoundIDs.isNotEmpty) {
_logger.severe("Could not find products: " +
response.notFoundIDs.toString());
@ -331,8 +364,8 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
_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());
@ -400,5 +433,22 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
),
);
}
}
Future<void> _launchFamilyPortal() async {
await _dialog.show();
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');
},
));
} catch (e) {
await _dialog.hide();
showGenericErrorDialog(context);
}
await _dialog.hide();
}
}

View file

@ -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<DetailsSectionWidget> {
void initState() {
super.initState();
_fetchUserDetails();
_userDetailsChangedEvent = Bus.instance.on<UserDetailsChangedEvent>().listen((event) {
_userDetailsChangedEvent =
Bus.instance.on<UserDetailsChangedEvent>().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<DetailsSectionWidget> {
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<DetailsSectionWidget> {
chartRadius: 80,
ringStrokeWidth: 4,
chartType: ChartType.ring,
centerText:
convertBytesToReadableFormat(_userDetails.usage) + "\nused",
centerText: convertBytesToReadableFormat(
_userDetails.getPersonalUsage()) +
"\nused",
centerTextStyle: TextStyle(
color: Colors.white,
fontSize: 12,