Fix events on sign in
This commit is contained in:
parent
a05dfbab22
commit
8d7cbc7c01
15 changed files with 24 additions and 1213 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
@ -14,7 +14,7 @@
|
|||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["--dart-define", "endpoint=http://192.168.1.33:8080"]
|
||||
"args": ["--dart-define", "endpoint=http://192.168.1.3:8080"]
|
||||
},
|
||||
{
|
||||
"name": "Prod",
|
||||
|
|
|
@ -6,8 +6,8 @@ import 'package:adaptive_theme/adaptive_theme.dart';
|
|||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/events/account_configured_event.dart';
|
||||
import 'package:ente_auth/events/user_logged_out_event.dart';
|
||||
import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/events/signed_out_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import "package:ente_auth/onboarding/view/onboarding_page.dart";
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
|
@ -24,18 +24,17 @@ class App extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _AppState extends State<App> {
|
||||
StreamSubscription<UserLoggedOutEvent> _loggedOutEvent;
|
||||
StreamSubscription<AccountConfiguredEvent> _accountConfiguredEvent;
|
||||
StreamSubscription<SignedOutEvent> _signedOutEvent;
|
||||
StreamSubscription<SignedInEvent> _signedInEvent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_loggedOutEvent = Bus.instance.on<UserLoggedOutEvent>().listen((event) {
|
||||
_signedOutEvent = Bus.instance.on<SignedOutEvent>().listen((event) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
_accountConfiguredEvent =
|
||||
Bus.instance.on<AccountConfiguredEvent>().listen((event) {
|
||||
_signedInEvent = Bus.instance.on<SignedInEvent>().listen((event) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
|
@ -46,8 +45,8 @@ class _AppState extends State<App> {
|
|||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_loggedOutEvent.cancel();
|
||||
_accountConfiguredEvent.cancel();
|
||||
_signedOutEvent.cancel();
|
||||
_signedInEvent.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -8,7 +8,7 @@ import 'package:ente_auth/core/constants.dart';
|
|||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/events/user_logged_out_event.dart';
|
||||
import 'package:ente_auth/events/signed_out_event.dart';
|
||||
import 'package:ente_auth/models/key_attributes.dart';
|
||||
import 'package:ente_auth/models/key_gen_result.dart';
|
||||
import 'package:ente_auth/models/private_key_attributes.dart';
|
||||
|
@ -130,7 +130,7 @@ class Configuration {
|
|||
_key = null;
|
||||
_cachedToken = null;
|
||||
_secretKey = null;
|
||||
Bus.instance.fire(UserLoggedOutEvent());
|
||||
Bus.instance.fire(SignedOutEvent());
|
||||
}
|
||||
|
||||
Future<KeyGenResult> generateKey(String password) async {
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import 'package:ente_auth/events/event.dart';
|
||||
|
||||
class AccountConfiguredEvent extends Event {}
|
|
@ -1,3 +1,3 @@
|
|||
import 'package:ente_auth/events/event.dart';
|
||||
|
||||
class UserLoggedOutEvent extends Event {}
|
||||
class SignedOutEvent extends Event {}
|
|
@ -1,3 +0,0 @@
|
|||
import 'package:ente_auth/events/event.dart';
|
||||
|
||||
class SubscriptionPurchasedEvent extends Event {}
|
|
@ -1,7 +0,0 @@
|
|||
import 'package:ente_auth/events/event.dart';
|
||||
|
||||
class TwoFactorStatusChangeEvent extends Event {
|
||||
final bool status;
|
||||
|
||||
TwoFactorStatusChangeEvent(this.status);
|
||||
}
|
|
@ -6,7 +6,8 @@ import 'package:ente_auth/core/configuration.dart';
|
|||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/events/account_configured_event.dart';
|
||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/gateway/authenticator.dart';
|
||||
import 'package:ente_auth/models/authenticator/auth_entity.dart';
|
||||
import 'package:ente_auth/models/authenticator/auth_key.dart';
|
||||
|
@ -38,7 +39,7 @@ class AuthenticatorService {
|
|||
if (Configuration.instance.hasConfiguredAccount()) {
|
||||
unawaited(sync());
|
||||
}
|
||||
Bus.instance.on<AccountConfiguredEvent>().listen((event) {
|
||||
Bus.instance.on<SignedInEvent>().listen((event) {
|
||||
unawaited(sync());
|
||||
});
|
||||
}
|
||||
|
@ -107,7 +108,10 @@ class AuthenticatorService {
|
|||
try {
|
||||
_logger.info("Sync");
|
||||
await _remoteToLocalSync();
|
||||
_logger.info("remote fetch completed");
|
||||
await _localToRemoteSync();
|
||||
_logger.info("local push completed");
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
} catch (e) {
|
||||
_logger.severe("Failed to sync with remote", e);
|
||||
}
|
||||
|
@ -120,8 +124,9 @@ class AuthenticatorService {
|
|||
final List<AuthEntity> result =
|
||||
await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
|
||||
if (result.isEmpty) {
|
||||
_logger.info('remote fetch completed');
|
||||
return;
|
||||
} else {
|
||||
_logger.info(result.length.toString() + " entries fetched from remote");
|
||||
}
|
||||
final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
|
||||
List<String> deletedIDs =
|
||||
|
@ -138,11 +143,14 @@ class AuthenticatorService {
|
|||
}
|
||||
|
||||
Future<void> _localToRemoteSync() async {
|
||||
_logger.info('Initiating local to remote sync');
|
||||
_logger.info('Initiating local to remote sync');
|
||||
final List<LocalAuthEntity> result = await _db.getAll();
|
||||
final List<LocalAuthEntity> pendingUpdate = result
|
||||
.where((element) => element.shouldSync || element.id == null)
|
||||
.toList();
|
||||
_logger.info(
|
||||
pendingUpdate.length.toString() + " entries to be updated at remote",
|
||||
);
|
||||
for (LocalAuthEntity entity in pendingUpdate) {
|
||||
if (entity.id == null) {
|
||||
_logger.info("Adding new entry");
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/account_configured_event.dart';
|
||||
import 'package:ente_auth/events/subscription_purchased_event.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/account/recovery_key_page.dart';
|
||||
import 'package:ente_auth/ui/common/dynamic_fab.dart';
|
||||
|
@ -393,7 +390,6 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
showShortToast(context, "Password changed successfully");
|
||||
Navigator.of(context).pop();
|
||||
if (widget.mode == PasswordEntryMode.reset) {
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
} catch (e, s) {
|
||||
|
@ -417,7 +413,6 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
|||
try {
|
||||
await UserService.instance.setAttributes(result);
|
||||
await dialog.hide();
|
||||
Bus.instance.fire(AccountConfiguredEvent());
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/subscription_purchased_event.dart';
|
||||
import 'package:ente_auth/models/key_attributes.dart';
|
||||
import 'package:ente_auth/ui/account/recovery_page.dart';
|
||||
import 'package:ente_auth/ui/common/dialogs.dart';
|
||||
|
@ -127,7 +125,6 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
|
|||
return;
|
||||
}
|
||||
await dialog.hide();
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/subscription_purchased_event.dart';
|
||||
import 'package:ente_auth/models/billing_plan.dart';
|
||||
import 'package:ente_auth/models/subscription.dart';
|
||||
import 'package:ente_auth/onboarding/view/onboarding_page.dart';
|
||||
import 'package:ente_auth/services/billing_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SkipSubscriptionWidget extends StatelessWidget {
|
||||
const SkipSubscriptionWidget({
|
||||
Key key,
|
||||
@required this.freePlan,
|
||||
}) : super(key: key);
|
||||
|
||||
final FreePlan freePlan;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 64,
|
||||
margin: const EdgeInsets.fromLTRB(0, 30, 0, 0),
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: OutlinedButton(
|
||||
style: Theme.of(context).outlinedButtonTheme.style.copyWith(
|
||||
textStyle: MaterialStateProperty.resolveWith<TextStyle>(
|
||||
(Set<MaterialState> states) {
|
||||
return Theme.of(context).textTheme.subtitle1;
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const OnboardingPage();
|
||||
},
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
BillingService.instance
|
||||
.verifySubscription(freeProductID, "", paymentProvider: "ente");
|
||||
},
|
||||
child: const Text("Continue on free plan"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,571 +0,0 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/models/billing_plan.dart';
|
||||
import 'package:ente_auth/models/subscription.dart';
|
||||
import 'package:ente_auth/models/user_details.dart';
|
||||
import 'package:ente_auth/services/billing_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/common/bottom_shadow.dart';
|
||||
import 'package:ente_auth/ui/common/dialogs.dart';
|
||||
import 'package:ente_auth/ui/common/loading_widget.dart';
|
||||
import 'package:ente_auth/ui/common/progress_dialog.dart';
|
||||
import 'package:ente_auth/ui/common/web_page.dart';
|
||||
import 'package:ente_auth/ui/payment/child_subscription_widget.dart';
|
||||
import 'package:ente_auth/ui/payment/payment_web_page.dart';
|
||||
import 'package:ente_auth/ui/payment/skip_subscription_widget.dart';
|
||||
import 'package:ente_auth/ui/payment/subscription_common_widgets.dart';
|
||||
import 'package:ente_auth/ui/payment/subscription_plan_widget.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:step_progress_indicator/step_progress_indicator.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class StripeSubscriptionPage extends StatefulWidget {
|
||||
final bool isOnboarding;
|
||||
|
||||
const StripeSubscriptionPage({
|
||||
this.isOnboarding = false,
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StripeSubscriptionPage> createState() => _StripeSubscriptionPageState();
|
||||
}
|
||||
|
||||
class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
||||
final _logger = Logger("StripeSubscriptionPage");
|
||||
final _billingService = BillingService.instance;
|
||||
final _userService = UserService.instance;
|
||||
Subscription _currentSubscription;
|
||||
ProgressDialog _dialog;
|
||||
UserDetails _userDetails;
|
||||
|
||||
// indicates if user's subscription plan is still active
|
||||
bool _hasActiveSubscription;
|
||||
FreePlan _freePlan;
|
||||
List<BillingPlan> _plans = [];
|
||||
bool _hasLoadedData = false;
|
||||
bool _isLoading = false;
|
||||
bool _isStripeSubscriber = false;
|
||||
bool _showYearlyPlan = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _fetchSub() async {
|
||||
return _userService
|
||||
.getUserDetailsV2(memoryCount: false)
|
||||
.then((userDetails) async {
|
||||
_userDetails = userDetails;
|
||||
_currentSubscription = userDetails.subscription;
|
||||
_showYearlyPlan = _currentSubscription.isYearlyPlan();
|
||||
_hasActiveSubscription = _currentSubscription.isValid();
|
||||
_isStripeSubscriber = _currentSubscription.paymentProvider == stripe;
|
||||
return _filterStripeForUI().then((value) {
|
||||
_hasLoadedData = true;
|
||||
setState(() {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// _filterPlansForUI is used for initializing initState & plan toggle states
|
||||
Future<void> _filterStripeForUI() async {
|
||||
final billingPlans = await _billingService.getBillingPlans();
|
||||
_freePlan = billingPlans.freePlan;
|
||||
_plans = billingPlans.plans.where((plan) {
|
||||
if (plan.stripeID == null || plan.stripeID.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final isYearlyPlan = plan.period == 'year';
|
||||
return isYearlyPlan == _showYearlyPlan;
|
||||
}).toList();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
FutureOr onWebPaymentGoBack(dynamic value) async {
|
||||
// refresh subscription
|
||||
await _dialog.show();
|
||||
try {
|
||||
await _fetchSub();
|
||||
} catch (e) {
|
||||
showToast(context, "Failed to refresh subscription");
|
||||
}
|
||||
await _dialog.hide();
|
||||
|
||||
// verify user has subscribed before redirecting to main page
|
||||
if (widget.isOnboarding &&
|
||||
_currentSubscription != null &&
|
||||
_currentSubscription.isValid() &&
|
||||
_currentSubscription.productID != freeProductID) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appBar = PreferredSize(
|
||||
preferredSize: const Size(double.infinity, 60),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 8),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: widget.isOnboarding
|
||||
? AppBar(
|
||||
elevation: 0,
|
||||
title: Hero(
|
||||
tag: "subscription",
|
||||
child: StepProgressIndicator(
|
||||
totalSteps: 4,
|
||||
currentStep: 4,
|
||||
selectedColor:
|
||||
Theme.of(context).colorScheme.alternativeColor,
|
||||
roundedEdges: const Radius.circular(10),
|
||||
unselectedColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.stepProgressUnselectedColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
: AppBar(
|
||||
elevation: 0,
|
||||
title: const Text("Subscription"),
|
||||
),
|
||||
),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
body: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
_getBody(),
|
||||
const BottomShadowWidget(
|
||||
offsetDy: 40,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
if (!_isLoading) {
|
||||
_isLoading = true;
|
||||
_dialog = createProgressDialog(context, "Please wait...");
|
||||
_fetchSub();
|
||||
}
|
||||
if (_hasLoadedData) {
|
||||
if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) {
|
||||
return ChildSubscriptionWidget(userDetails: _userDetails);
|
||||
} else {
|
||||
return _buildPlans();
|
||||
}
|
||||
}
|
||||
return const EnteLoadingWidget();
|
||||
}
|
||||
|
||||
Widget _buildPlans() {
|
||||
final widgets = <Widget>[];
|
||||
|
||||
widgets.add(
|
||||
SubscriptionHeaderWidget(
|
||||
isOnboarding: widget.isOnboarding,
|
||||
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
|
||||
),
|
||||
);
|
||||
|
||||
widgets.addAll([
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _getStripePlanWidgets(),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
]);
|
||||
|
||||
widgets.add(_showSubscriptionToggle());
|
||||
|
||||
if (_hasActiveSubscription) {
|
||||
widgets.add(ValidityWidget(currentSubscription: _currentSubscription));
|
||||
}
|
||||
|
||||
if (_currentSubscription.productID == freeProductID) {
|
||||
if (widget.isOnboarding) {
|
||||
widgets.add(SkipSubscriptionWidget(freePlan: _freePlan));
|
||||
}
|
||||
widgets.add(const SubFaqWidget());
|
||||
}
|
||||
|
||||
// only active subscription can be renewed/canceled
|
||||
if (_hasActiveSubscription && _isStripeSubscriber) {
|
||||
widgets.add(_stripeRenewOrCancelButton());
|
||||
}
|
||||
|
||||
if (_currentSubscription.productID != freeProductID) {
|
||||
widgets.addAll([
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final String paymentProvider =
|
||||
_currentSubscription.paymentProvider;
|
||||
switch (_currentSubscription.paymentProvider) {
|
||||
case stripe:
|
||||
await _launchStripePortal();
|
||||
break;
|
||||
case playStore:
|
||||
launchUrlString(
|
||||
"https://play.google.com/store/account/subscriptions?sku=" +
|
||||
_currentSubscription.productID +
|
||||
"&package=io.ente.auth",
|
||||
);
|
||||
break;
|
||||
case appStore:
|
||||
launchUrlString("https://apps.apple.com/account/billing");
|
||||
break;
|
||||
default:
|
||||
final String capitalizedWord = paymentProvider.isNotEmpty
|
||||
? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
|
||||
: '';
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"Please contact us at support@ente.io to manage your "
|
||||
"$capitalizedWord subscription.",
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(40, 80, 40, 20),
|
||||
child: Column(
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: "Payment details",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontFamily: 'Inter-Medium',
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!widget.isOnboarding) {
|
||||
widgets.addAll([
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
await _launchFamilyPortal();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(40, 0, 40, 80),
|
||||
child: Column(
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: "Manage family",
|
||||
style: Theme.of(context).textTheme.overline,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: widgets,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _launchStripePortal() async {
|
||||
await _dialog.show();
|
||||
try {
|
||||
final 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();
|
||||
}
|
||||
|
||||
Future<void> _launchFamilyPortal() async {
|
||||
if (_userDetails.subscription.productID == freeProductID) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
"Now you can share your storage plan with your family members!",
|
||||
"Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
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&isFamilyCreated=$familyExist',
|
||||
);
|
||||
},
|
||||
),
|
||||
).then((value) => onWebPaymentGoBack);
|
||||
} catch (e) {
|
||||
await _dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
}
|
||||
await _dialog.hide();
|
||||
}
|
||||
|
||||
Widget _stripeRenewOrCancelButton() {
|
||||
final bool isRenewCancelled =
|
||||
_currentSubscription.attributes?.isCancelled ?? false;
|
||||
final String title =
|
||||
isRenewCancelled ? "Renew subscription" : "Cancel subscription";
|
||||
return TextButton(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: (isRenewCancelled
|
||||
? Colors.greenAccent
|
||||
: Theme.of(context).colorScheme.onSurface)
|
||||
.withOpacity(isRenewCancelled ? 1.0 : 0.2),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
bool confirmAction = false;
|
||||
if (isRenewCancelled) {
|
||||
final choice = await showChoiceDialog(
|
||||
context,
|
||||
title,
|
||||
"Are you sure you want to renew?",
|
||||
firstAction: "No",
|
||||
secondAction: "Yes",
|
||||
);
|
||||
confirmAction = choice == DialogUserChoice.secondChoice;
|
||||
} else {
|
||||
final choice = await showChoiceDialog(
|
||||
context,
|
||||
title,
|
||||
'Are you sure you want to cancel?',
|
||||
firstAction: 'Yes, cancel',
|
||||
secondAction: 'No',
|
||||
actionType: ActionType.critical,
|
||||
);
|
||||
confirmAction = choice == DialogUserChoice.firstChoice;
|
||||
}
|
||||
if (confirmAction) {
|
||||
toggleStripeSubscription(isRenewCancelled);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleStripeSubscription(bool isRenewCancelled) async {
|
||||
await _dialog.show();
|
||||
try {
|
||||
isRenewCancelled
|
||||
? await _billingService.activateStripeSubscription()
|
||||
: await _billingService.cancelStripeSubscription();
|
||||
await _fetchSub();
|
||||
} catch (e) {
|
||||
showToast(
|
||||
context,
|
||||
isRenewCancelled ? 'failed to renew' : 'failed to cancel',
|
||||
);
|
||||
}
|
||||
await _dialog.hide();
|
||||
}
|
||||
|
||||
List<Widget> _getStripePlanWidgets() {
|
||||
final List<Widget> planWidgets = [];
|
||||
bool foundActivePlan = false;
|
||||
for (final plan in _plans) {
|
||||
final productID = plan.stripeID;
|
||||
if (productID == null || productID.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final isActive =
|
||||
_hasActiveSubscription && _currentSubscription.productID == productID;
|
||||
if (isActive) {
|
||||
foundActivePlan = true;
|
||||
}
|
||||
planWidgets.add(
|
||||
Material(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
if (isActive) {
|
||||
return;
|
||||
}
|
||||
// prompt user to cancel their active subscription form other
|
||||
// payment providers
|
||||
if (!_isStripeSubscriber &&
|
||||
_hasActiveSubscription &&
|
||||
_currentSubscription.productID != freeProductID) {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"Please cancel your existing subscription from "
|
||||
"${_currentSubscription.paymentProvider} first",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_userDetails.getFamilyOrPersonalUsage() > plan.storage) {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"You cannot downgrade to this plan",
|
||||
);
|
||||
return;
|
||||
}
|
||||
String stripPurChaseAction = 'buy';
|
||||
if (_isStripeSubscriber && _hasActiveSubscription) {
|
||||
// confirm if user wants to change plan or not
|
||||
final result = await showChoiceDialog(
|
||||
context,
|
||||
"Confirm plan change",
|
||||
"Are you sure you want to change your plan?",
|
||||
firstAction: "No",
|
||||
secondAction: 'Yes',
|
||||
);
|
||||
if (result != DialogUserChoice.secondChoice) {
|
||||
return;
|
||||
}
|
||||
stripPurChaseAction = 'update';
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return PaymentWebPage(
|
||||
planId: plan.stripeID,
|
||||
actionType: stripPurChaseAction,
|
||||
);
|
||||
},
|
||||
),
|
||||
).then((value) => onWebPaymentGoBack(value));
|
||||
},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: plan.storage,
|
||||
price: plan.price,
|
||||
period: plan.period,
|
||||
isActive: isActive,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!foundActivePlan && _hasActiveSubscription) {
|
||||
_addCurrentPlanWidget(planWidgets);
|
||||
}
|
||||
return planWidgets;
|
||||
}
|
||||
|
||||
Widget _showSubscriptionToggle() {
|
||||
Widget _planText(String title, bool reduceOpacity) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 4, right: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(reduceOpacity ? 0.5 : 1.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
// color: Color.fromRGBO(10, 40, 40, 0.3),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_planText("Monthly", _showYearlyPlan),
|
||||
Switch(
|
||||
value: _showYearlyPlan,
|
||||
activeColor: Colors.white,
|
||||
inactiveThumbColor: Colors.white,
|
||||
onChanged: (value) async {
|
||||
_showYearlyPlan = value;
|
||||
await _filterStripeForUI();
|
||||
},
|
||||
),
|
||||
_planText("Yearly", !_showYearlyPlan)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addCurrentPlanWidget(List<Widget> planWidgets) {
|
||||
// don't add current plan if it's monthly plan but UI is showing yearly plans
|
||||
// and vice versa.
|
||||
if (_showYearlyPlan != _currentSubscription.isYearlyPlan() &&
|
||||
_currentSubscription.productID != freeProductID) {
|
||||
return;
|
||||
}
|
||||
int activePlanIndex = 0;
|
||||
for (; activePlanIndex < _plans.length; activePlanIndex++) {
|
||||
if (_plans[activePlanIndex].storage > _currentSubscription.storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
planWidgets.insert(
|
||||
activePlanIndex,
|
||||
Material(
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: _currentSubscription.storage,
|
||||
price: _currentSubscription.price,
|
||||
period: _currentSubscription.period,
|
||||
isActive: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// @dart=2.9
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/ui/payment/stripe_subscription_page.dart';
|
||||
import 'package:ente_auth/ui/payment/subscription_page.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
StatefulWidget getSubscriptionPage({bool isOnBoarding = false}) {
|
||||
if (UpdateService.instance.isIndependentFlavor()) {
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
}
|
||||
if (_isUserCreatedPostStripeSupport()) {
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
} else {
|
||||
return SubscriptionPage(isOnboarding: isOnBoarding);
|
||||
}
|
||||
}
|
||||
|
||||
// return true if the user was created after we added support for stripe payment
|
||||
// on frame. We do this check to avoid showing Stripe payment option for earlier
|
||||
// users who might have paid via playStore. This method should be removed once
|
||||
// we have better handling for active play/app store subscription & stripe plans.
|
||||
bool _isUserCreatedPostStripeSupport() {
|
||||
return Configuration.instance.getUserID() > 1580559962386460;
|
||||
}
|
|
@ -1,516 +0,0 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/subscription_purchased_event.dart';
|
||||
import 'package:ente_auth/models/billing_plan.dart';
|
||||
import 'package:ente_auth/models/subscription.dart';
|
||||
import 'package:ente_auth/models/user_details.dart';
|
||||
import 'package:ente_auth/services/billing_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/common/loading_widget.dart';
|
||||
import 'package:ente_auth/ui/common/progress_dialog.dart';
|
||||
import 'package:ente_auth/ui/common/web_page.dart';
|
||||
import 'package:ente_auth/ui/payment/child_subscription_widget.dart';
|
||||
import 'package:ente_auth/ui/payment/skip_subscription_widget.dart';
|
||||
import 'package:ente_auth/ui/payment/subscription_common_widgets.dart';
|
||||
import 'package:ente_auth/ui/payment/subscription_plan_widget.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class SubscriptionPage extends StatefulWidget {
|
||||
final bool isOnboarding;
|
||||
|
||||
const SubscriptionPage({
|
||||
this.isOnboarding = false,
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
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;
|
||||
UserDetails _userDetails;
|
||||
bool _hasActiveSubscription;
|
||||
FreePlan _freePlan;
|
||||
List<BillingPlan> _plans;
|
||||
bool _hasLoadedData = false;
|
||||
bool _isLoading = false;
|
||||
bool _isActiveStripeSubscriber;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_billingService.setIsOnSubscriptionPage(true);
|
||||
_setupPurchaseUpdateStreamListener();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setupPurchaseUpdateStreamListener() {
|
||||
_purchaseUpdateSubscription = InAppPurchaseConnection
|
||||
.instance.purchaseUpdatedStream
|
||||
.listen((purchases) async {
|
||||
if (!_dialog.isShowing()) {
|
||||
await _dialog.show();
|
||||
}
|
||||
for (final purchase in purchases) {
|
||||
_logger.info("Purchase status " + purchase.status.toString());
|
||||
if (purchase.status == PurchaseStatus.purchased) {
|
||||
try {
|
||||
final newSubscription = await _billingService.verifySubscription(
|
||||
purchase.productID,
|
||||
purchase.verificationData.serverVerificationData,
|
||||
);
|
||||
await InAppPurchaseConnection.instance.completePurchase(purchase);
|
||||
String text = "Thank you for subscribing!";
|
||||
if (!widget.isOnboarding) {
|
||||
final isUpgrade = _hasActiveSubscription &&
|
||||
newSubscription.storage > _currentSubscription.storage;
|
||||
final isDowngrade = _hasActiveSubscription &&
|
||||
newSubscription.storage < _currentSubscription.storage;
|
||||
if (isUpgrade) {
|
||||
text = "Your plan was successfully upgraded";
|
||||
} else if (isDowngrade) {
|
||||
text = "Your plan was successfully downgraded";
|
||||
}
|
||||
}
|
||||
showToast(context, text);
|
||||
_currentSubscription = newSubscription;
|
||||
_hasActiveSubscription = _currentSubscription.isValid();
|
||||
setState(() {});
|
||||
await _dialog.hide();
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
if (widget.isOnboarding) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
} on SubscriptionAlreadyClaimedError catch (e) {
|
||||
_logger.warning("subscription is already claimed ", e);
|
||||
await _dialog.hide();
|
||||
final String title = "${Platform.isAndroid ? "Play" : "App"}"
|
||||
"Store subscription";
|
||||
final String id =
|
||||
Platform.isAndroid ? "Google Play ID" : "Apple ID";
|
||||
final String message = '''Your $id is already linked to another
|
||||
ente account.\nIf you would like to use your $id with this
|
||||
account, please contact our support''';
|
||||
showErrorDialog(context, title, message);
|
||||
return;
|
||||
} catch (e) {
|
||||
_logger.warning("Could not complete payment ", e);
|
||||
await _dialog.hide();
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Payment failed",
|
||||
"Please talk to " +
|
||||
(Platform.isAndroid ? "PlayStore" : "AppStore") +
|
||||
" support if you were charged",
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
|
||||
await InAppPurchaseConnection.instance.completePurchase(purchase);
|
||||
await _dialog.hide();
|
||||
} else if (purchase.status == PurchaseStatus.error) {
|
||||
await _dialog.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_purchaseUpdateSubscription.cancel();
|
||||
_billingService.setIsOnSubscriptionPage(false);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_isLoading) {
|
||||
_isLoading = true;
|
||||
_fetchSubData();
|
||||
}
|
||||
_dialog = createProgressDialog(context, "Please wait...");
|
||||
final appBar = AppBar(
|
||||
title: widget.isOnboarding ? null : const Text("Subscription"),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
body: _getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _fetchSubData() async {
|
||||
_userService.getUserDetailsV2(memoryCount: false).then((userDetails) async {
|
||||
_userDetails = userDetails;
|
||||
_currentSubscription = userDetails.subscription;
|
||||
_hasActiveSubscription = _currentSubscription.isValid();
|
||||
final billingPlans = await _billingService.getBillingPlans();
|
||||
_isActiveStripeSubscriber =
|
||||
_currentSubscription.paymentProvider == stripe &&
|
||||
_currentSubscription.isValid();
|
||||
_plans = billingPlans.plans.where((plan) {
|
||||
final productID = _isActiveStripeSubscriber
|
||||
? plan.stripeID
|
||||
: Platform.isAndroid
|
||||
? plan.androidID
|
||||
: plan.iosID;
|
||||
return productID != null && productID.isNotEmpty;
|
||||
}).toList();
|
||||
_freePlan = billingPlans.freePlan;
|
||||
_hasLoadedData = true;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
if (_hasLoadedData) {
|
||||
if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) {
|
||||
return ChildSubscriptionWidget(userDetails: _userDetails);
|
||||
} else {
|
||||
return _buildPlans();
|
||||
}
|
||||
}
|
||||
return const EnteLoadingWidget();
|
||||
}
|
||||
|
||||
Widget _buildPlans() {
|
||||
final widgets = <Widget>[];
|
||||
widgets.add(
|
||||
SubscriptionHeaderWidget(
|
||||
isOnboarding: widget.isOnboarding,
|
||||
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
|
||||
),
|
||||
);
|
||||
|
||||
widgets.addAll([
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _isActiveStripeSubscriber
|
||||
? _getStripePlanWidgets()
|
||||
: _getMobilePlanWidgets(),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
]);
|
||||
|
||||
if (_hasActiveSubscription) {
|
||||
widgets.add(ValidityWidget(currentSubscription: _currentSubscription));
|
||||
}
|
||||
|
||||
if (_currentSubscription.productID == freeProductID) {
|
||||
if (widget.isOnboarding) {
|
||||
widgets.add(SkipSubscriptionWidget(freePlan: _freePlan));
|
||||
}
|
||||
widgets.add(const SubFaqWidget());
|
||||
}
|
||||
|
||||
if (_hasActiveSubscription &&
|
||||
_currentSubscription.productID != freeProductID) {
|
||||
widgets.addAll([
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
final String paymentProvider =
|
||||
_currentSubscription.paymentProvider;
|
||||
if (paymentProvider == appStore && !Platform.isAndroid) {
|
||||
launchUrlString("https://apps.apple.com/account/billing");
|
||||
} else if (paymentProvider == playStore && Platform.isAndroid) {
|
||||
launchUrlString(
|
||||
"https://play.google.com/store/account/subscriptions?sku=" +
|
||||
_currentSubscription.productID +
|
||||
"&package=io.ente.auth",
|
||||
);
|
||||
} else if (paymentProvider == stripe) {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"Visit web.ente.io to manage your subscription",
|
||||
);
|
||||
} else {
|
||||
final String capitalizedWord = paymentProvider.isNotEmpty
|
||||
? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
|
||||
: '';
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"Please contact us at support@ente.io to manage your "
|
||||
"$capitalizedWord subscription.",
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(40, 80, 40, 20),
|
||||
child: Column(
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: _isActiveStripeSubscriber
|
||||
? "Visit web.ente.io to manage your subscription"
|
||||
: "Payment details",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontFamily: 'Inter-Medium',
|
||||
fontSize: 14,
|
||||
decoration: _isActiveStripeSubscriber
|
||||
? TextDecoration.none
|
||||
: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
if (!widget.isOnboarding) {
|
||||
widgets.addAll([
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
_launchFamilyPortal();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(40, 0, 40, 80),
|
||||
child: Column(
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: "Manage family",
|
||||
style: Theme.of(context).textTheme.overline,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: widgets,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _getStripePlanWidgets() {
|
||||
final List<Widget> planWidgets = [];
|
||||
bool foundActivePlan = false;
|
||||
for (final plan in _plans) {
|
||||
final productID = plan.stripeID;
|
||||
if (productID == null || productID.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final isActive =
|
||||
_hasActiveSubscription && _currentSubscription.productID == productID;
|
||||
if (isActive) {
|
||||
foundActivePlan = true;
|
||||
}
|
||||
planWidgets.add(
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
if (isActive) {
|
||||
return;
|
||||
}
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"Please visit web.ente.io to manage your subscription",
|
||||
);
|
||||
},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: plan.storage,
|
||||
price: plan.price,
|
||||
period: plan.period,
|
||||
isActive: isActive,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!foundActivePlan && _hasActiveSubscription) {
|
||||
_addCurrentPlanWidget(planWidgets);
|
||||
}
|
||||
return planWidgets;
|
||||
}
|
||||
|
||||
List<Widget> _getMobilePlanWidgets() {
|
||||
bool foundActivePlan = false;
|
||||
final List<Widget> planWidgets = [];
|
||||
if (_hasActiveSubscription &&
|
||||
_currentSubscription.productID == freeProductID) {
|
||||
foundActivePlan = true;
|
||||
planWidgets.add(
|
||||
SubscriptionPlanWidget(
|
||||
storage: _freePlan.storage,
|
||||
price: "free",
|
||||
period: "",
|
||||
isActive: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final plan in _plans) {
|
||||
final productID = Platform.isAndroid ? plan.androidID : plan.iosID;
|
||||
final isActive =
|
||||
_hasActiveSubscription && _currentSubscription.productID == productID;
|
||||
if (isActive) {
|
||||
foundActivePlan = true;
|
||||
}
|
||||
planWidgets.add(
|
||||
Material(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
if (isActive) {
|
||||
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});
|
||||
if (response.notFoundIDs.isNotEmpty) {
|
||||
_logger.severe(
|
||||
"Could not find products: " + response.notFoundIDs.toString(),
|
||||
);
|
||||
await _dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
return;
|
||||
}
|
||||
final isCrossGradingOnAndroid = Platform.isAndroid &&
|
||||
_hasActiveSubscription &&
|
||||
_currentSubscription.productID != freeProductID &&
|
||||
_currentSubscription.productID != plan.androidID;
|
||||
if (isCrossGradingOnAndroid) {
|
||||
final existingProductDetailsResponse =
|
||||
await InAppPurchaseConnection.instance
|
||||
.queryProductDetails({_currentSubscription.productID});
|
||||
if (existingProductDetailsResponse.notFoundIDs.isNotEmpty) {
|
||||
_logger.severe(
|
||||
"Could not find existing products: " +
|
||||
response.notFoundIDs.toString(),
|
||||
);
|
||||
await _dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
return;
|
||||
}
|
||||
final subscriptionChangeParam = ChangeSubscriptionParam(
|
||||
oldPurchaseDetails: PurchaseDetails(
|
||||
purchaseID: null,
|
||||
productID: _currentSubscription.productID,
|
||||
verificationData: null,
|
||||
transactionDate: null,
|
||||
),
|
||||
);
|
||||
await InAppPurchaseConnection.instance.buyNonConsumable(
|
||||
purchaseParam: PurchaseParam(
|
||||
productDetails: response.productDetails[0],
|
||||
changeSubscriptionParam: subscriptionChangeParam,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await InAppPurchaseConnection.instance.buyNonConsumable(
|
||||
purchaseParam: PurchaseParam(
|
||||
productDetails: response.productDetails[0],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: plan.storage,
|
||||
price: plan.price,
|
||||
period: plan.period,
|
||||
isActive: isActive,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!foundActivePlan && _hasActiveSubscription) {
|
||||
_addCurrentPlanWidget(planWidgets);
|
||||
}
|
||||
return planWidgets;
|
||||
}
|
||||
|
||||
void _addCurrentPlanWidget(List<Widget> planWidgets) {
|
||||
int activePlanIndex = 0;
|
||||
for (; activePlanIndex < _plans.length; activePlanIndex++) {
|
||||
if (_plans[activePlanIndex].storage > _currentSubscription.storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
planWidgets.insert(
|
||||
activePlanIndex,
|
||||
Material(
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: _currentSubscription.storage,
|
||||
price: _currentSubscription.price,
|
||||
period: _currentSubscription.period,
|
||||
isActive: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// todo: refactor manage family in common widget
|
||||
Future<void> _launchFamilyPortal() async {
|
||||
if (_userDetails.subscription.productID == freeProductID) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
"Share your storage plan with your family members!",
|
||||
"Customers on paid plans can add up to 5 family members without paying extra. Each member gets their own private space.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
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&isFamilyCreated=$familyExist',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
await _dialog.hide();
|
||||
showGenericErrorDialog(context);
|
||||
}
|
||||
await _dialog.hide();
|
||||
}
|
||||
}
|
|
@ -1,12 +1,9 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/events/two_factor_status_change_event.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/account/sessions_page.dart';
|
||||
|
@ -28,22 +25,13 @@ class SecuritySectionWidget extends StatefulWidget {
|
|||
class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
final _config = Configuration.instance;
|
||||
|
||||
StreamSubscription<TwoFactorStatusChangeEvent> _twoFactorStatusChangeEvent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_twoFactorStatusChangeEvent =
|
||||
Bus.instance.on<TwoFactorStatusChangeEvent>().listen((event) async {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_twoFactorStatusChangeEvent.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue