diff --git a/.vscode/launch.json b/.vscode/launch.json index b90852637..615eb8c1b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index c883fe7a5..b8cb82629 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -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 { - StreamSubscription _loggedOutEvent; - StreamSubscription _accountConfiguredEvent; + StreamSubscription _signedOutEvent; + StreamSubscription _signedInEvent; @override void initState() { - _loggedOutEvent = Bus.instance.on().listen((event) { + _signedOutEvent = Bus.instance.on().listen((event) { if (mounted) { setState(() {}); } }); - _accountConfiguredEvent = - Bus.instance.on().listen((event) { + _signedInEvent = Bus.instance.on().listen((event) { if (mounted) { setState(() {}); } @@ -46,8 +45,8 @@ class _AppState extends State { @override void dispose() { super.dispose(); - _loggedOutEvent.cancel(); - _accountConfiguredEvent.cancel(); + _signedOutEvent.cancel(); + _signedInEvent.cancel(); } @override diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index f26ea5134..cce52cd22 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -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 generateKey(String password) async { diff --git a/lib/events/account_configured_event.dart b/lib/events/account_configured_event.dart deleted file mode 100644 index c8c7c55ed..000000000 --- a/lib/events/account_configured_event.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:ente_auth/events/event.dart'; - -class AccountConfiguredEvent extends Event {} diff --git a/lib/events/user_logged_out_event.dart b/lib/events/signed_out_event.dart similarity index 52% rename from lib/events/user_logged_out_event.dart rename to lib/events/signed_out_event.dart index 2908965a3..abe492770 100644 --- a/lib/events/user_logged_out_event.dart +++ b/lib/events/signed_out_event.dart @@ -1,3 +1,3 @@ import 'package:ente_auth/events/event.dart'; -class UserLoggedOutEvent extends Event {} +class SignedOutEvent extends Event {} diff --git a/lib/events/subscription_purchased_event.dart b/lib/events/subscription_purchased_event.dart deleted file mode 100644 index e20b32f06..000000000 --- a/lib/events/subscription_purchased_event.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:ente_auth/events/event.dart'; - -class SubscriptionPurchasedEvent extends Event {} diff --git a/lib/events/two_factor_status_change_event.dart b/lib/events/two_factor_status_change_event.dart deleted file mode 100644 index c963a198e..000000000 --- a/lib/events/two_factor_status_change_event.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:ente_auth/events/event.dart'; - -class TwoFactorStatusChangeEvent extends Event { - final bool status; - - TwoFactorStatusChangeEvent(this.status); -} diff --git a/lib/services/authenticator_service.dart b/lib/services/authenticator_service.dart index 5419b5c4f..a6a9a8eec 100644 --- a/lib/services/authenticator_service.dart +++ b/lib/services/authenticator_service.dart @@ -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().listen((event) { + Bus.instance.on().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 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 deletedIDs = @@ -138,11 +143,14 @@ class AuthenticatorService { } Future _localToRemoteSync() async { - _logger.info('Initiating local to remote sync'); + _logger.info('Initiating local to remote sync'); final List result = await _db.getAll(); final List 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"); diff --git a/lib/ui/account/password_entry_page.dart b/lib/ui/account/password_entry_page.dart index 31cafd838..928cfc94e 100644 --- a/lib/ui/account/password_entry_page.dart +++ b/lib/ui/account/password_entry_page.dart @@ -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 { 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 { try { await UserService.instance.setAttributes(result); await dialog.hide(); - Bus.instance.fire(AccountConfiguredEvent()); Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { diff --git a/lib/ui/account/password_reentry_page.dart b/lib/ui/account/password_reentry_page.dart index cd26ef315..d4f35b5e3 100644 --- a/lib/ui/account/password_reentry_page.dart +++ b/lib/ui/account/password_reentry_page.dart @@ -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 { return; } await dialog.hide(); - Bus.instance.fire(SubscriptionPurchasedEvent()); Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { diff --git a/lib/ui/payment/skip_subscription_widget.dart b/lib/ui/payment/skip_subscription_widget.dart deleted file mode 100644 index 074761d3b..000000000 --- a/lib/ui/payment/skip_subscription_widget.dart +++ /dev/null @@ -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( - (Set 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"), - ), - ); - } -} diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart deleted file mode 100644 index 697e6796a..000000000 --- a/lib/ui/payment/stripe_subscription_page.dart +++ /dev/null @@ -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 createState() => _StripeSubscriptionPageState(); -} - -class _StripeSubscriptionPageState extends State { - 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 _plans = []; - bool _hasLoadedData = false; - bool _isLoading = false; - bool _isStripeSubscriber = false; - bool _showYearlyPlan = false; - - @override - void initState() { - super.initState(); - } - - Future _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 _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 = []; - - 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 _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 _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 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 _getStripePlanWidgets() { - final List 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 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, - ), - ), - ), - ); - } -} diff --git a/lib/ui/payment/subscription.dart b/lib/ui/payment/subscription.dart deleted file mode 100644 index ddc9b8f63..000000000 --- a/lib/ui/payment/subscription.dart +++ /dev/null @@ -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; -} diff --git a/lib/ui/payment/subscription_page.dart b/lib/ui/payment/subscription_page.dart deleted file mode 100644 index 1453e5bab..000000000 --- a/lib/ui/payment/subscription_page.dart +++ /dev/null @@ -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 createState() => _SubscriptionPageState(); -} - -class _SubscriptionPageState extends State { - 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 _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 _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 = []; - 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 _getStripePlanWidgets() { - final List 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 _getMobilePlanWidgets() { - bool foundActivePlan = false; - final List 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 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 _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(); - } -} diff --git a/lib/ui/settings/security_section_widget.dart b/lib/ui/settings/security_section_widget.dart index 85eefa7a6..b9aae3fb1 100644 --- a/lib/ui/settings/security_section_widget.dart +++ b/lib/ui/settings/security_section_widget.dart @@ -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 { final _config = Configuration.instance; - StreamSubscription _twoFactorStatusChangeEvent; - @override void initState() { super.initState(); - _twoFactorStatusChangeEvent = - Bus.instance.on().listen((event) async { - if (mounted) { - setState(() {}); - } - }); } @override void dispose() { - _twoFactorStatusChangeEvent.cancel(); super.dispose(); }