diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c35a3149b..a1557af98 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5321,5 +5321,9 @@ "inviteFriends": "Invite friends", "activityStatsButtonTooltip": "Activity info", "allow": "Allow", - "deny": "Deny" + "deny": "Deny", + "enabledRenewal": "Enable Subscription Renewal", + "subscriptionEndsOn": "Subscription Ends On", + "subscriptionRenewsOn": "Subscription Renews On", + "waitForSubscriptionChanges": "Changes to your subscription may take a moment to reflect in the app." } \ No newline at end of file diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index bd402ab4d..37bdbd7b9 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -525,7 +525,6 @@ class ChatListController extends State //#Pangea StreamSubscription? _invitedSpaceSubscription; - StreamSubscription? _subscriptionStatusStream; StreamSubscription? _roomCapacitySubscription; //Pangea# @@ -613,13 +612,8 @@ class ChatListController extends State } }); - _subscriptionStatusStream ??= MatrixState - .pangeaController.subscriptionController.subscriptionStream.stream - .listen((event) { - if (mounted) { - showSubscribedSnackbar(context); - } - }); + MatrixState.pangeaController.subscriptionController.subscriptionNotifier + .addListener(_onSubscribe); // listen for space child updates for any space that is not the active space // so that when the user navigates to the space that was updated, it will @@ -673,6 +667,10 @@ class ChatListController extends State } // #Pangea + void _onSubscribe() { + if (mounted) showSubscribedSnackbar(context); + } + Future _joinInvitedSpaces() async { final invitedSpaces = Matrix.of(context).client.rooms.where( (r) => r.isSpace && r.membership == Membership.invite, @@ -691,8 +689,9 @@ class ChatListController extends State _intentUriStreamSubscription?.cancel(); //#Pangea _invitedSpaceSubscription?.cancel(); - _subscriptionStatusStream?.cancel(); _roomCapacitySubscription?.cancel(); + MatrixState.pangeaController.subscriptionController.subscriptionNotifier + .removeListener(_onSubscribe); //Pangea# scrollController.removeListener(_onScroll); super.dispose(); diff --git a/lib/pangea/common/constants/local.key.dart b/lib/pangea/common/constants/local.key.dart index 8c5d84061..9d722ca6c 100644 --- a/lib/pangea/common/constants/local.key.dart +++ b/lib/pangea/common/constants/local.key.dart @@ -4,6 +4,7 @@ class PLocalKey { static const String beganWebPayment = "beganWebPayment"; static const String dismissedPaywall = 'dismissedPaywall'; static const String paywallBackoff = 'paywallBackoff'; + static const String clickedCancelSubscription = 'clickedCancelSubscription'; static const String messagesSinceUpdate = 'messagesSinceLastUpdate'; static const String completedActivities = 'completedActivities'; static const String justInputtedCode = 'justInputtedCode'; diff --git a/lib/pangea/subscription/controllers/subscription_controller.dart b/lib/pangea/subscription/controllers/subscription_controller.dart index cca7bf4e0..16c126992 100644 --- a/lib/pangea/subscription/controllers/subscription_controller.dart +++ b/lib/pangea/subscription/controllers/subscription_controller.dart @@ -12,7 +12,6 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/common/controllers/base_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; import 'package:fluffychat/pangea/common/network/urls.dart'; @@ -35,13 +34,13 @@ enum SubscriptionStatus { shouldShowPaywall, } -class SubscriptionController extends BaseController { +class SubscriptionController with ChangeNotifier { late PangeaController _pangeaController; CurrentSubscriptionInfo? currentSubscriptionInfo; AvailableSubscriptionsInfo? availableSubscriptionInfo; - final StreamController subscriptionStream = StreamController.broadcast(); + final ValueNotifier subscriptionNotifier = ValueNotifier(false); SubscriptionController(PangeaController pangeaController) : super() { _pangeaController = pangeaController; @@ -119,22 +118,20 @@ class SubscriptionController extends BaseController { (CustomerInfo info) async { final bool? wasSubscribed = isSubscribed; await updateCustomerInfo(); - if (wasSubscribed != null && - !wasSubscribed && - (isSubscribed != null && isSubscribed!)) { - subscriptionStream.add(true); + if (wasSubscribed == false && isSubscribed == true) { + subscriptionNotifier.value = true; } }, ); } else { if (SubscriptionManagementRepo.getBeganWebPayment()) { await SubscriptionManagementRepo.removeBeganWebPayment(); - if (isSubscribed != null && isSubscribed!) { - subscriptionStream.add(true); + if (isSubscribed == true) { + subscriptionNotifier.value = true; } } } - setState(null); + notifyListeners(); } catch (e, s) { debugPrint("Failed to initialize subscription controller"); ErrorHandler.logError( @@ -197,7 +194,6 @@ class SubscriptionController extends BaseController { isPromo: isPromo, ); await SubscriptionManagementRepo.setBeganWebPayment(); - setState(null); launchUrlString( paymentLink, webOnlyWindowName: "_self", @@ -234,7 +230,7 @@ class SubscriptionController extends BaseController { Future updateCustomerInfo() async { await currentSubscriptionInfo?.setCurrentSubscription(); - setState(null); + notifyListeners(); } /// if the user is subscribed, returns subscribed diff --git a/lib/pangea/subscription/models/base_subscription_info.dart b/lib/pangea/subscription/models/base_subscription_info.dart index a9ae276a0..61f6cae0f 100644 --- a/lib/pangea/subscription/models/base_subscription_info.dart +++ b/lib/pangea/subscription/models/base_subscription_info.dart @@ -11,6 +11,7 @@ class CurrentSubscriptionInfo { final AvailableSubscriptionsInfo availableSubscriptionInfo; DateTime? expirationDate; + DateTime? unsubscribeDetectedAt; String? currentSubscriptionId; CurrentSubscriptionInfo({ @@ -59,6 +60,9 @@ class CurrentSubscriptionInfo { (currentSubscription?.appId == availableSubscriptionInfo.appIds?.currentAppId); + DateTime? get subscriptionEndDate => + unsubscribeDetectedAt == null ? null : expirationDate; + void resetSubscription() => currentSubscriptionId = null; Future setCurrentSubscription() async {} } diff --git a/lib/pangea/subscription/models/mobile_subscriptions.dart b/lib/pangea/subscription/models/mobile_subscriptions.dart index 8d45469ae..23a73de64 100644 --- a/lib/pangea/subscription/models/mobile_subscriptions.dart +++ b/lib/pangea/subscription/models/mobile_subscriptions.dart @@ -104,10 +104,10 @@ class MobileSubscriptionInfo extends CurrentSubscriptionInfo { expirationDate = activeEntitlement.expirationDate != null ? DateTime.parse(activeEntitlement.expirationDate!) : null; + unsubscribeDetectedAt = activeEntitlement.unsubscribeDetectedAt != null + ? DateTime.parse(activeEntitlement.unsubscribeDetectedAt!) + : null; - if (activeEntitlement.periodType == PeriodType.trial) { - // We dont use actual trials as it would require adding a CC on devices - } if (currentSubscriptionId != null && currentSubscription == null) { Sentry.addBreadcrumb( Breadcrumb(message: "mismatch of productIds and currentSubscriptionID"), diff --git a/lib/pangea/subscription/models/web_subscriptions.dart b/lib/pangea/subscription/models/web_subscriptions.dart index 6f25f3513..2ceb36d11 100644 --- a/lib/pangea/subscription/models/web_subscriptions.dart +++ b/lib/pangea/subscription/models/web_subscriptions.dart @@ -21,7 +21,16 @@ class WebSubscriptionInfo extends CurrentSubscriptionInfo { ); currentSubscriptionId = rcResponse.currentSubscriptionId; - expirationDate = rcResponse.expirationDate; + final currentSubscription = + rcResponse.allSubscriptions?[currentSubscriptionId]; + + if (currentSubscription != null) { + expirationDate = DateTime.tryParse(currentSubscription.expiresDate); + unsubscribeDetectedAt = + currentSubscription.unsubscribeDetectedAt != null + ? DateTime.parse(currentSubscription.unsubscribeDetectedAt!) + : null; + } } catch (err) { currentSubscriptionId = AppConfig.errorSubscriptionId; } diff --git a/lib/pangea/subscription/pages/change_subscription.dart b/lib/pangea/subscription/pages/change_subscription.dart index a124abf09..d7a291c95 100644 --- a/lib/pangea/subscription/pages/change_subscription.dart +++ b/lib/pangea/subscription/pages/change_subscription.dart @@ -171,21 +171,20 @@ class ChangeSubscription extends StatelessWidget { ElevatedButton( onPressed: () => controller .submitChange(subscription), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - controller.loading - ? const CircularProgressIndicator - .adaptive() - : Text( + child: controller.loading + ? const LinearProgressIndicator() + : Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( subscription.isTrial ? L10n.of(context) .activateTrial : L10n.of(context).pay, ), - ], - ), + ], + ), ), ], ), diff --git a/lib/pangea/subscription/pages/settings_subscription.dart b/lib/pangea/subscription/pages/settings_subscription.dart index 8def4f50d..a1f54d8ca 100644 --- a/lib/pangea/subscription/pages/settings_subscription.dart +++ b/lib/pangea/subscription/pages/settings_subscription.dart @@ -2,16 +2,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/subscription/pages/settings_subscription_view.dart'; +import 'package:fluffychat/pangea/subscription/repo/subscription_management_repo.dart'; import 'package:fluffychat/pangea/subscription/utils/subscription_app_id.dart'; import 'package:fluffychat/pangea/subscription/widgets/subscription_snackbar.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; class SubscriptionManagement extends StatefulWidget { @@ -27,37 +27,25 @@ class SubscriptionManagementController extends State { MatrixState.pangeaController.subscriptionController; SubscriptionDetails? selectedSubscription; - StreamSubscription? _subscriptionStatusStream; bool loading = false; - late StreamSubscription _settingsSubscription; - @override void initState() { if (!subscriptionController.initCompleter.isCompleted) { subscriptionController.initialize().then((_) => setState(() {})); } - _settingsSubscription = subscriptionController.stateStream.listen((event) { - debugPrint("stateStream event in subscription settings"); - setState(() {}); - }); - - _subscriptionStatusStream ??= - subscriptionController.subscriptionStream.stream.listen((_) { - showSubscribedSnackbar(context); - context.go('/rooms'); - }); - + subscriptionController.addListener(_onSubscriptionUpdate); + subscriptionController.subscriptionNotifier.addListener(_onSubscribe); subscriptionController.updateCustomerInfo(); super.initState(); } @override void dispose() { + subscriptionController.subscriptionNotifier.removeListener(_onSubscribe); + subscriptionController.removeListener(_onSubscriptionUpdate); super.dispose(); - _settingsSubscription.cancel(); - _subscriptionStatusStream?.cancel(); } bool get subscriptionsAvailable => @@ -106,29 +94,46 @@ class SubscriptionManagementController extends State { .currentSubscriptionInfo!.currentPlatformMatchesPurchasePlatform; } + DateTime? get expirationDate => + subscriptionController.currentSubscriptionInfo?.expirationDate; + + DateTime? get subscriptionEndDate => + subscriptionController.currentSubscriptionInfo?.subscriptionEndDate; + + void _onSubscriptionUpdate() => setState(() {}); + void _onSubscribe() => showSubscribedSnackbar(context); + Future submitChange( SubscriptionDetails subscription, { bool isPromo = false, }) async { setState(() => loading = true); - await showFutureLoadingDialog( - context: context, - future: () async => subscriptionController.submitSubscriptionChange( + try { + await subscriptionController.submitSubscriptionChange( subscription, context, isPromo: isPromo, - ), - onError: (error, s) { - setState(() => loading = false); - return null; - }, - ); - - if (mounted && loading) { - setState(() => loading = false); + ); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "subscription_id": subscription.id, + "is_promo": isPromo, + }, + ); + } finally { + if (mounted) setState(() => loading = false); } } + Future onClickCancelSubscription() async { + await SubscriptionManagementRepo.setClickedCancelSubscription(); + await launchMangementUrl(ManagementOption.cancel); + if (mounted) setState(() {}); + } + Future launchMangementUrl(ManagementOption option) async { String managementUrl = Environment.stripeManagementUrl; final String? email = diff --git a/lib/pangea/subscription/pages/settings_subscription_view.dart b/lib/pangea/subscription/pages/settings_subscription_view.dart index a7cb9e412..c5b3bb506 100644 --- a/lib/pangea/subscription/pages/settings_subscription_view.dart +++ b/lib/pangea/subscription/pages/settings_subscription_view.dart @@ -7,6 +7,7 @@ import 'package:intl/intl.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/subscription/pages/change_subscription.dart'; import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart'; +import 'package:fluffychat/pangea/subscription/repo/subscription_management_repo.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; class SettingsSubscriptionView extends StatelessWidget { @@ -25,12 +26,18 @@ class SettingsSubscriptionView extends StatelessWidget { Column( children: [ ListTile( - title: Text(L10n.of(context).cancelSubscription), - enabled: controller.showManagementOptions, - onTap: () => controller.launchMangementUrl( - ManagementOption.cancel, + title: Text( + controller.subscriptionEndDate == null + ? L10n.of(context).cancelSubscription + : L10n.of(context).enabledRenewal, + ), + enabled: controller.showManagementOptions, + onTap: controller.onClickCancelSubscription, + trailing: Icon( + controller.subscriptionEndDate == null + ? Icons.cancel_outlined + : Icons.refresh_outlined, ), - trailing: const Icon(Icons.cancel_outlined), ), const Divider(height: 1), ListTile( @@ -49,6 +56,42 @@ class SettingsSubscriptionView extends StatelessWidget { ), enabled: controller.showManagementOptions, ), + if (controller.expirationDate != null) ...[ + const Divider(height: 1), + ListTile( + title: Text( + controller.subscriptionEndDate != null + ? L10n.of(context).subscriptionEndsOn + : L10n.of(context).subscriptionRenewsOn, + ), + subtitle: Text( + DateFormat.yMMMMd().format( + controller.expirationDate!.toLocal(), + ), + ), + ), + if (SubscriptionManagementRepo.getClickedCancelSubscription()) + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.info_outline, + size: 20, + ), + Flexible( + child: Text( + L10n.of(context).waitForSubscriptionChanges, + textAlign: TextAlign.center, + style: const TextStyle(fontStyle: FontStyle.italic), + ), + ), + ], + ), + ), + ], ], ), ]; @@ -68,16 +111,15 @@ class SettingsSubscriptionView extends StatelessWidget { child: Column( children: [ if (isSubscribed == null) - const Center(child: CircularProgressIndicator.adaptive()), - if (isSubscribed != null && - isSubscribed && - !controller.showManagementOptions) + const Center(child: CircularProgressIndicator.adaptive()) + else if (isSubscribed && !controller.showManagementOptions) ManagementNotAvailableWarning( controller: controller, - ), - if (isSubscribed != null && !isSubscribed) + ) + else if (isSubscribed && controller.showManagementOptions) + ...managementButtons + else ChangeSubscription(controller: controller), - if (controller.showManagementOptions) ...managementButtons, ], ), ), diff --git a/lib/pangea/subscription/repo/subscription_management_repo.dart b/lib/pangea/subscription/repo/subscription_management_repo.dart index c25d572d3..f0b96a678 100644 --- a/lib/pangea/subscription/repo/subscription_management_repo.dart +++ b/lib/pangea/subscription/repo/subscription_management_repo.dart @@ -69,4 +69,22 @@ class SubscriptionManagementRepo { final int backoff = _getPaywallBackoff() + 1; await _cache.write(PLocalKey.paywallBackoff, backoff); } + + static Future setClickedCancelSubscription() async { + await _cache.write( + PLocalKey.clickedCancelSubscription, + DateTime.now().toIso8601String(), + ); + } + + static bool getClickedCancelSubscription() { + final entry = _cache.read(PLocalKey.clickedCancelSubscription); + if (entry == null) return false; + final val = DateTime.tryParse(entry); + return val != null && DateTime.now().difference(val).inSeconds < 60; + } + + static Future removeClickedCancelSubscription() async { + await _cache.remove(PLocalKey.clickedCancelSubscription); + } } diff --git a/lib/pangea/subscription/repo/subscription_repo.dart b/lib/pangea/subscription/repo/subscription_repo.dart index 24ecfed98..f844a20b5 100644 --- a/lib/pangea/subscription/repo/subscription_repo.dart +++ b/lib/pangea/subscription/repo/subscription_repo.dart @@ -151,7 +151,6 @@ class RCProductsResponseModel { class RCSubscriptionResponseModel { String? currentSubscriptionId; SubscriptionDetails? currentSubscription; - DateTime? expirationDate; List? allEntitlements; Map? allSubscriptions; @@ -159,7 +158,6 @@ class RCSubscriptionResponseModel { this.currentSubscriptionId, this.currentSubscription, this.allEntitlements, - this.expirationDate, this.allSubscriptions, }); @@ -188,14 +186,6 @@ class RCSubscriptionResponseModel { } final String currentSubscriptionId = activeEntitlements[0]; - - final Map currentSubscriptionMetadata = - json['subscriptions'][currentSubscriptionId]; - - final DateTime expirationDate = DateTime.parse( - currentSubscriptionMetadata['expires_date'], - ); - final SubscriptionDetails? currentSubscription = allProducts?.firstWhereOrNull( (SubscriptionDetails sub) => @@ -206,7 +196,6 @@ class RCSubscriptionResponseModel { return RCSubscriptionResponseModel( currentSubscription: currentSubscription, currentSubscriptionId: currentSubscriptionId, - expirationDate: expirationDate, allEntitlements: activeEntitlements, allSubscriptions: history, );