commit
714e88b22d
9 changed files with 472 additions and 77 deletions
BIN
assets/family_sharing.jpg
Normal file
BIN
assets/family_sharing.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
148
lib/ui/payment/child_subscription_widget.dart
Normal file
148
lib/ui/payment/child_subscription_widget.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue