diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index 859e33e89..f065e833b 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -54,8 +54,9 @@ class SettingsSecurityController extends State { // #Pangea final subscriptionController = MatrixState.pangeaController.subscriptionController; - if (subscriptionController.subscription?.isPaidSubscription == true && - subscriptionController.subscription?.defaultManagementURL != null) { + if (subscriptionController.currentSubscriptionInfo?.isPaidSubscription == + true && + subscriptionController.defaultManagementURL != null) { final resp = await showOkCancelAlertDialog( useRootNavigator: false, context: context, @@ -66,7 +67,7 @@ class SettingsSecurityController extends State { ); if (resp == OkCancelResult.ok) { launchUrlString( - subscriptionController.subscription!.defaultManagementURL!, + subscriptionController.defaultManagementURL!, mode: LaunchMode.externalApplication, ); return; diff --git a/lib/pangea/controllers/subscription_controller.dart b/lib/pangea/controllers/subscription_controller.dart index 94c396cab..61cfc2f88 100644 --- a/lib/pangea/controllers/subscription_controller.dart +++ b/lib/pangea/controllers/subscription_controller.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'dart:convert'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/user_controller.dart'; import 'package:fluffychat/pangea/models/base_subscription_info.dart'; import 'package:fluffychat/pangea/models/mobile_subscriptions.dart'; import 'package:fluffychat/pangea/models/web_subscriptions.dart'; @@ -13,6 +14,7 @@ import 'package:fluffychat/pangea/network/requests.dart'; import 'package:fluffychat/pangea/network/urls.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/pangea/utils/subscription_app_id.dart'; import 'package:fluffychat/pangea/widgets/subscription/subscription_paywall.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/foundation.dart'; @@ -31,7 +33,10 @@ enum SubscriptionStatus { class SubscriptionController extends BaseController { late PangeaController _pangeaController; - SubscriptionInfo? subscription; + + CurrentSubscriptionInfo? currentSubscriptionInfo; + AvailableSubscriptionsInfo? availableSubscriptionInfo; + final StreamController subscriptionStream = StreamController.broadcast(); final StreamController trialActivationStream = StreamController.broadcast(); @@ -39,10 +44,11 @@ class SubscriptionController extends BaseController { _pangeaController = pangeaController; } + UserController get userController => _pangeaController.userController; + String? get userID => _pangeaController.matrixState.client.userID; + bool get isSubscribed => - subscription != null && - (subscription!.currentSubscriptionId != null || - subscription!.currentSubscription != null); + currentSubscriptionInfo?.currentSubscriptionId != null; bool _isInitializing = false; Completer initialized = Completer(); @@ -67,18 +73,27 @@ class SubscriptionController extends BaseController { Future _initialize() async { try { - if (_pangeaController.matrixState.client.userID == null) { + if (userID == null) { debugPrint( "Attempted to initalize subscription information with null userId", ); return; } - subscription = kIsWeb - ? WebSubscriptionInfo(pangeaController: _pangeaController) - : MobileSubscriptionInfo(pangeaController: _pangeaController); + availableSubscriptionInfo = AvailableSubscriptionsInfo(); + await availableSubscriptionInfo!.setAvailableSubscriptions(); - await subscription!.configure(); + currentSubscriptionInfo = kIsWeb + ? WebSubscriptionInfo( + userID: userID!, + availableSubscriptionInfo: availableSubscriptionInfo!, + ) + : MobileSubscriptionInfo( + userID: userID!, + availableSubscriptionInfo: availableSubscriptionInfo!, + ); + + await currentSubscriptionInfo!.configure(); if (_activatedNewUserTrial) { setNewUserTrial(); } @@ -101,7 +116,7 @@ class SubscriptionController extends BaseController { await _pangeaController.pStoreService.delete( PLocalKey.beganWebPayment, ); - if (_pangeaController.subscriptionController.isSubscribed) { + if (isSubscribed) { subscriptionStream.add(true); } } @@ -170,7 +185,7 @@ class SubscriptionController extends BaseController { return; } ErrorHandler.logError( - m: "Failed to purchase revenuecat package for user ${_pangeaController.matrixState.client.userID} with error code $errCode", + m: "Failed to purchase revenuecat package for user $userID with error code $errCode", s: StackTrace.current, ); return; @@ -178,14 +193,19 @@ class SubscriptionController extends BaseController { } } - bool get _activatedNewUserTrial { - final bool activated = _pangeaController - .userController.profile.userSettings.activatedFreeTrial; - return _pangeaController.userController.inTrialWindow && activated; - } + int get currentTrialDays => userController.inTrialWindow(trialDays: 1) + ? 1 + : userController.inTrialWindow(trialDays: 7) + ? 7 + : 0; + + bool get _activatedNewUserTrial => + userController.inTrialWindow(trialDays: 1) || + (userController.inTrialWindow() && + userController.profile.userSettings.activatedFreeTrial); void activateNewUserTrial() { - _pangeaController.userController.updateProfile( + userController.updateProfile( (profile) { profile.userSettings.activatedFreeTrial = true; return profile; @@ -196,8 +216,7 @@ class SubscriptionController extends BaseController { } void setNewUserTrial() { - final DateTime? createdAt = - _pangeaController.userController.profile.userSettings.createdAt; + final DateTime? createdAt = userController.profile.userSettings.createdAt; if (createdAt == null) { ErrorHandler.logError( m: "Null user profile createAt in subscription settings", @@ -207,23 +226,16 @@ class SubscriptionController extends BaseController { } final DateTime expirationDate = createdAt.add( - const Duration(days: 7), + Duration(days: currentTrialDays), ); - subscription?.setTrial(expirationDate); + currentSubscriptionInfo?.setTrial(expirationDate); } Future updateCustomerInfo() async { if (!initialized.isCompleted) { await initialize(); } - if (subscription == null) { - ErrorHandler.logError( - m: "Null subscription info in subscription settings", - s: StackTrace.current, - ); - return; - } - await subscription!.setCustomerInfo(); + await currentSubscriptionInfo!.setCurrentSubscription(); setState(null); } @@ -284,7 +296,7 @@ class SubscriptionController extends BaseController { if (!initialized.isCompleted) { await initialize(); } - if (subscription?.availableSubscriptions.isEmpty ?? true) { + if (availableSubscriptionInfo?.availableSubscriptions.isEmpty ?? true) { return; } if (isSubscribed) return; @@ -310,70 +322,51 @@ class SubscriptionController extends BaseController { } } - Future getPaymentLink(String duration, {bool isPromo = false}) async { + Future getPaymentLink( + SubscriptionDuration duration, { + bool isPromo = false, + }) async { final Requests req = Requests(baseUrl: PApiUrls.baseAPI); final String reqUrl = Uri.encodeFull( - "${PApiUrls.paymentLink}?pangea_user_id=${_pangeaController.matrixState.client.userID}&duration=$duration&redeem=$isPromo", + "${PApiUrls.paymentLink}?pangea_user_id=$userID&duration=${duration.value}&redeem=$isPromo", ); final Response res = await req.get(url: reqUrl); final json = jsonDecode(res.body); String paymentLink = json["link"]["url"]; - final String? email = await _pangeaController.userController.userEmail; + final String? email = await userController.userEmail; if (email != null) { paymentLink += "?prefilled_email=${Uri.encodeComponent(email)}"; } return paymentLink; } - Future fetchSubscriptionStatus() async { - final Requests req = Requests(baseUrl: PApiUrls.baseAPI); - final String reqUrl = Uri.encodeFull( - "${PApiUrls.subscriptionExpiration}?pangea_user_id=${_pangeaController.matrixState.client.userID}", - ); + String? get defaultManagementURL => + currentSubscriptionInfo?.currentSubscription + ?.defaultManagementURL(availableSubscriptionInfo?.appIds); +} - DateTime? expiration; - try { - final Response res = await req.get(url: reqUrl); - final json = jsonDecode(res.body); - if (json["premium_expires_date"] != null) { - expiration = DateTime.parse(json["premium_expires_date"]); - } - } catch (err) { - ErrorHandler.logError( - e: "Failed to fetch subscripton status for user ${_pangeaController.matrixState.client.userID}", - s: StackTrace.current, - ); - } - final bool subscribed = - expiration == null ? false : DateTime.now().isBefore(expiration); - GoogleAnalytics.updateUserSubscriptionStatus(subscribed); - return subscribed; - } +enum SubscriptionPeriodType { + normal, + trial, +} - Future redeemPromoCode(BuildContext context) async { - final List? promoCode = await showTextInputDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.enterPromoCode, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [const DialogTextField()], - ); - if (promoCode == null || promoCode.single.isEmpty) return; - launchUrlString( - "${AppConfig.iosPromoCode}${promoCode.single}", - ); - } +enum SubscriptionDuration { + month, + year, +} + +extension SubscriptionDurationExtension on SubscriptionDuration { + String get value => this == SubscriptionDuration.month ? "month" : "year"; } class SubscriptionDetails { - double price; - String? duration; - Package? package; - String? appId; + final double price; + final SubscriptionDuration? duration; + final String? appId; final String id; - String? periodType = "normal"; + SubscriptionPeriodType periodType; + Package? package; SubscriptionDetails({ required this.price, @@ -381,30 +374,35 @@ class SubscriptionDetails { this.duration, this.package, this.appId, - this.periodType, + this.periodType = SubscriptionPeriodType.normal, }); - void makeTrial() => periodType = 'trial'; - bool get isTrial => periodType == 'trial'; + void makeTrial() => periodType = SubscriptionPeriodType.trial; + bool get isTrial => periodType == SubscriptionPeriodType.trial; - String displayPrice(BuildContext context) { - if (isTrial || price <= 0) { - return L10n.of(context)!.freeTrial; - } - return "\$${price.toStringAsFixed(2)}"; - } + String displayPrice(BuildContext context) => isTrial || price <= 0 + ? L10n.of(context)!.freeTrial + : "\$${price.toStringAsFixed(2)}"; String displayName(BuildContext context) { if (isTrial) { return L10n.of(context)!.oneWeekTrial; } switch (duration) { - case ('month'): + case (SubscriptionDuration.month): return L10n.of(context)!.monthlySubscription; - case ('year'): + case (SubscriptionDuration.year): return L10n.of(context)!.yearlySubscription; default: return L10n.of(context)!.defaultSubscription; } } + + String? defaultManagementURL(SubscriptionAppIds? appIds) { + return appId == appIds?.androidId + ? AppConfig.googlePlayMangementUrl + : appId == appIds?.appleId + ? AppConfig.appleMangementUrl + : Environment.stripeManagementUrl; + } } diff --git a/lib/pangea/controllers/user_controller.dart b/lib/pangea/controllers/user_controller.dart index ca7e2869a..381c08c4d 100644 --- a/lib/pangea/controllers/user_controller.dart +++ b/lib/pangea/controllers/user_controller.dart @@ -196,13 +196,13 @@ class UserController extends BaseController { } /// Returns a boolean value indicating whether the user is currently in the trial window. - bool get inTrialWindow { + bool inTrialWindow({int trialDays = 7}) { final DateTime? createdAt = profile.userSettings.createdAt; if (createdAt == null) { return false; } return createdAt.isAfter( - DateTime.now().subtract(const Duration(days: 7)), + DateTime.now().subtract(Duration(days: trialDays)), ); } diff --git a/lib/pangea/models/base_subscription_info.dart b/lib/pangea/models/base_subscription_info.dart index 469ac5786..eda6d8a4b 100644 --- a/lib/pangea/models/base_subscription_info.dart +++ b/lib/pangea/models/base_subscription_info.dart @@ -1,51 +1,33 @@ +import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/config/environment.dart'; -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/repo/subscription_repo.dart'; import 'package:fluffychat/pangea/utils/subscription_app_id.dart'; -class SubscriptionInfo { - PangeaController pangeaController; - List availableSubscriptions = []; - String? currentSubscriptionId; - SubscriptionDetails? currentSubscription; - // Gabby - is it necessary to store appIds for each platform? - SubscriptionAppIds? appIds; - List? allProducts; - final SubscriptionPlatform platform = SubscriptionPlatform(); - List allEntitlements = []; +/// Contains information about the users's current subscription +class CurrentSubscriptionInfo { + final String userID; + final AvailableSubscriptionsInfo availableSubscriptionInfo; + DateTime? expirationDate; + String? currentSubscriptionId; - bool get hasSubscribed => allEntitlements.isNotEmpty; + CurrentSubscriptionInfo({ + required this.userID, + required this.availableSubscriptionInfo, + }); - SubscriptionInfo({ - required this.pangeaController, - }) : super(); + SubscriptionDetails? get currentSubscription { + if (currentSubscriptionId == null) return null; + return availableSubscriptionInfo.allProducts?.firstWhereOrNull( + (SubscriptionDetails sub) => + sub.id.contains(currentSubscriptionId!) || + currentSubscriptionId!.contains(sub.id), + ); + } Future configure() async {} - //TO-DO - hey Gabby this file feels like it could be reorganized. i'd like to - // 1) move these api calls to a class in a file in repo and - // 2) move the url to the urls file. - // 3) any stateful info to the subscription controller - // let's discuss before you make the changes though - // maybe you had some reason for this organization - - /* - Fetch App Ids for each RC app (iOS, Android, and Stripe). Used to determine which app a user - with an active subscription purchased that subscription. - */ - Future setAppIds() async { - if (appIds != null) return; - appIds = await SubscriptionRepo.getAppIds(); - } - - Future setAllProducts() async { - if (allProducts != null) return; - allProducts = await SubscriptionRepo.getAllProducts(); - } - bool get isNewUserTrial => currentSubscriptionId == AppConfig.trialSubscriptionId; @@ -64,41 +46,69 @@ class SubscriptionInfo { String? get purchasePlatformDisplayName { if (currentSubscription?.appId == null) return null; - return appIds?.appDisplayName(currentSubscription!.appId!); + return availableSubscriptionInfo.appIds + ?.appDisplayName(currentSubscription!.appId!); } bool get purchasedOnWeb => - (currentSubscription != null && appIds != null) && - (currentSubscription?.appId == appIds?.stripeId); + (currentSubscription != null && + availableSubscriptionInfo.appIds != null) && + (currentSubscription?.appId == + availableSubscriptionInfo.appIds?.stripeId); bool get currentPlatformMatchesPurchasePlatform => - (currentSubscription != null && appIds != null) && - (currentSubscription?.appId == appIds?.currentAppId); + (currentSubscription != null && + availableSubscriptionInfo.appIds != null) && + (currentSubscription?.appId == + availableSubscriptionInfo.appIds?.currentAppId); - void resetSubscription() { - currentSubscription = null; - currentSubscriptionId = null; - } + void resetSubscription() => currentSubscriptionId = null; void setTrial(DateTime expiration) { - if (currentSubscription != null) return; expirationDate = expiration; currentSubscriptionId = AppConfig.trialSubscriptionId; - currentSubscription = SubscriptionDetails( - price: 0, - id: AppConfig.trialSubscriptionId, - periodType: 'trial', - ); + if (currentSubscription == null) { + availableSubscriptionInfo.availableSubscriptions.add( + SubscriptionDetails( + price: 0, + id: AppConfig.trialSubscriptionId, + periodType: SubscriptionPeriodType.trial, + ), + ); + } } - Future setCustomerInfo() async {} + Future setCurrentSubscription() async {} +} - String? get defaultManagementURL { - final String? purchaseAppId = currentSubscription?.appId; - return purchaseAppId == appIds?.androidId - ? AppConfig.googlePlayMangementUrl - : purchaseAppId == appIds?.appleId - ? AppConfig.appleMangementUrl - : Environment.stripeManagementUrl; +/// Contains information about the suscriptions available on revenuecat +class AvailableSubscriptionsInfo { + List availableSubscriptions = []; + SubscriptionAppIds? appIds; + List? allProducts; + + Future setAvailableSubscriptions() async { + appIds ??= await SubscriptionRepo.getAppIds(); + allProducts ??= await SubscriptionRepo.getAllProducts(); + availableSubscriptions = (allProducts ?? []) + .where((product) => product.appId == appIds!.currentAppId) + .sorted((a, b) => a.price.compareTo(b.price)) + .toList(); + // //@Gabby - temporary solution to add trial to list + // if (currentSubscriptionId == null && !hasSubscribed) { + // final id = availableSubscriptions[0].id; + // final package = availableSubscriptions[0].package; + // final duration = availableSubscriptions[0].duration; + // availableSubscriptions.insert( + // 0, + // SubscriptionDetails( + // price: 0, + // id: id, + // duration: duration, + // package: package, + // periodType: SubscriptionPeriodType.trial, + // ), + // ); + // } } } diff --git a/lib/pangea/models/mobile_subscriptions.dart b/lib/pangea/models/mobile_subscriptions.dart index 00fc6e3fd..cf5685118 100644 --- a/lib/pangea/models/mobile_subscriptions.dart +++ b/lib/pangea/models/mobile_subscriptions.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/models/base_subscription_info.dart'; @@ -9,8 +8,11 @@ import 'package:flutter/material.dart'; import 'package:purchases_flutter/purchases_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -class MobileSubscriptionInfo extends SubscriptionInfo { - MobileSubscriptionInfo({required super.pangeaController}) : super(); +class MobileSubscriptionInfo extends CurrentSubscriptionInfo { + MobileSubscriptionInfo({ + required super.userID, + required super.availableSubscriptionInfo, + }); @override Future configure() async { @@ -19,112 +21,42 @@ class MobileSubscriptionInfo extends SubscriptionInfo { : PurchasesConfiguration(Environment.rcIosKey); try { await Purchases.configure( - configuration..appUserID = pangeaController.userController.userId, + configuration..appUserID = userID, ); + await super.configure(); + await setMobilePackages(); } catch (err) { ErrorHandler.logError( - m: "Failed to configure revenuecat SDK for user ${pangeaController.userController.userId}", + m: "Failed to configure revenuecat SDK", s: StackTrace.current, ); - debugPrint( - "Failed to configure revenuecat SDK for user ${pangeaController.userController.userId}", - ); - return; - } - await setAppIds(); - await setAllProducts(); - await setCustomerInfo(); - await setMobilePackages(); - if (allProducts != null && appIds != null) { - availableSubscriptions = allProducts! - .where((product) => product.appId == appIds!.currentAppId) - .toList(); - availableSubscriptions.sort((a, b) => a.price.compareTo(b.price)); - - if (currentSubscriptionId == null && !hasSubscribed) { - //@Gabby - temporary solution to add trial to list - final id = availableSubscriptions[0].id; - final package = availableSubscriptions[0].package; - final duration = availableSubscriptions[0].duration; - availableSubscriptions.insert( - 0, - SubscriptionDetails( - price: 0, - id: id, - duration: duration, - package: package, - periodType: 'trial', - ), - ); - } - } else { - ErrorHandler.logError(e: Exception("allProducts null || appIds null")); } } Future setMobilePackages() async { - if (allProducts == null) { - ErrorHandler.logError( - m: "Null appProducts in setMobilePrices", - s: StackTrace.current, - ); - debugPrint( - "Null appProducts in setMobilePrices", - ); - return; - } - Offerings offerings; - try { - offerings = await Purchases.getOfferings(); - } catch (err) { - ErrorHandler.logError( - m: "Failed to fetch revenuecat offerings from revenuecat", - s: StackTrace.current, - ); - debugPrint( - "Failed to fetch revenuecat offerings from revenuecat", - ); - return; - } + if (availableSubscriptionInfo.allProducts == null) return; + + final Offerings offerings = await Purchases.getOfferings(); final Offering? offering = offerings.all[Environment.rcOfferingName]; - if (offering != null) { - final List mobileSubscriptions = - offering.availablePackages - .map( - (package) { - return SubscriptionDetails( - price: package.storeProduct.price, - id: package.storeProduct.identifier, - package: package, - ); - }, - ) - .toList() - .cast(); - for (final SubscriptionDetails mobileSub in mobileSubscriptions) { - final int productIndex = allProducts! - .indexWhere((product) => product.id.contains(mobileSub.id)); - if (productIndex >= 0) { - final SubscriptionDetails updated = allProducts![productIndex]; - updated.package = mobileSub.package; - allProducts![productIndex] = updated; - } - } + if (offering == null) return; + + final products = availableSubscriptionInfo.allProducts; + for (final package in offering.availablePackages) { + final int productIndex = products!.indexWhere( + (product) => product.id.contains(package.storeProduct.identifier), + ); + + if (productIndex < 0) continue; + final SubscriptionDetails updated = + availableSubscriptionInfo.allProducts![productIndex]; + updated.package = package; + availableSubscriptionInfo.allProducts![productIndex] = updated; } } @override - Future setCustomerInfo() async { - if (allProducts == null) { - ErrorHandler.logError( - m: "Null allProducts in setCustomerInfo", - s: StackTrace.current, - ); - debugPrint( - "Null allProducts in setCustomerInfo", - ); - return; - } + Future setCurrentSubscription() async { + if (availableSubscriptionInfo.allProducts == null) return; CustomerInfo info; try { @@ -132,28 +64,11 @@ class MobileSubscriptionInfo extends SubscriptionInfo { info = await Purchases.getCustomerInfo(); } catch (err) { ErrorHandler.logError( - m: "Failed to fetch revenuecat customer info for user ${pangeaController.userController.userId}", + m: "Failed to fetch revenuecat customer info", s: StackTrace.current, ); - debugPrint( - "Failed to fetch revenuecat customer info for user ${pangeaController.userController.userId}", - ); return; } - final List noExpirations = - getEntitlementsWithoutExpiration(info); - - if (noExpirations.isNotEmpty) { - Sentry.addBreadcrumb( - Breadcrumb( - message: - "Found revenuecat entitlement(s) without expiration date for user ${pangeaController.userController.userId}: ${noExpirations.map( - (entry) => - "Entitlement Id: ${entry.identifier}, Purchase Date: ${entry.originalPurchaseDate}", - )}", - ), - ); - } final List activeEntitlements = info.entitlements.all.entries @@ -166,14 +81,6 @@ class MobileSubscriptionInfo extends SubscriptionInfo { .map((MapEntry entry) => entry.value) .toList(); - allEntitlements = info.entitlements.all.entries - .map( - (MapEntry entry) => - entry.value.productIdentifier, - ) - .cast() - .toList(); - if (activeEntitlements.length > 1) { debugPrint( "User has more than one active entitlement.", @@ -185,13 +92,9 @@ class MobileSubscriptionInfo extends SubscriptionInfo { } return; } + final EntitlementInfo activeEntitlement = activeEntitlements[0]; currentSubscriptionId = activeEntitlement.productIdentifier; - currentSubscription = allProducts!.firstWhereOrNull( - (SubscriptionDetails sub) => - sub.id.contains(currentSubscriptionId!) || - currentSubscriptionId!.contains(sub.id), - ); expirationDate = activeEntitlement.expirationDate != null ? DateTime.parse(activeEntitlement.expirationDate!) : null; @@ -205,15 +108,4 @@ class MobileSubscriptionInfo extends SubscriptionInfo { ); } } - - List getEntitlementsWithoutExpiration(CustomerInfo info) { - final List noExpirations = info.entitlements.all.entries - .where( - (MapEntry entry) => - entry.value.expirationDate == null, - ) - .map((MapEntry entry) => entry.value) - .toList(); - return noExpirations; - } } diff --git a/lib/pangea/models/web_subscriptions.dart b/lib/pangea/models/web_subscriptions.dart index 1a6cc722a..0f8362843 100644 --- a/lib/pangea/models/web_subscriptions.dart +++ b/lib/pangea/models/web_subscriptions.dart @@ -1,61 +1,23 @@ -import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/models/base_subscription_info.dart'; import 'package:fluffychat/pangea/repo/subscription_repo.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -class WebSubscriptionInfo extends SubscriptionInfo { - WebSubscriptionInfo({required super.pangeaController}) : super(); +class WebSubscriptionInfo extends CurrentSubscriptionInfo { + WebSubscriptionInfo({ + required super.userID, + required super.availableSubscriptionInfo, + }); @override - Future configure() async { - await setAppIds(); - await setAllProducts(); - await setCustomerInfo(); - - if (allProducts == null || appIds == null) { - Sentry.addBreadcrumb( - Breadcrumb(message: "No products found for current app"), - ); - return; - } - - availableSubscriptions = allProducts! - .where((product) => product.appId == appIds!.currentAppId) - .toList(); - availableSubscriptions.sort((a, b) => a.price.compareTo(b.price)); - //@Gabby - temporary solution to add trial to list - if (currentSubscriptionId == null && !hasSubscribed) { - final id = availableSubscriptions[0].id; - final package = availableSubscriptions[0].package; - final duration = availableSubscriptions[0].duration; - availableSubscriptions.insert( - 0, - SubscriptionDetails( - price: 0, - id: id, - duration: duration, - package: package, - periodType: 'trial', - ), - ); - } - } - - @override - Future setCustomerInfo() async { - if (currentSubscriptionId != null && currentSubscription != null) { - return; - } - final RCSubscriptionResponseModel currentSubscriptionInfo = - await SubscriptionRepo.getCurrentSubscriptionInfo( - pangeaController.matrixState.client.userID, - allProducts, + Future setCurrentSubscription() async { + if (currentSubscriptionId != null) return; + final rcResponse = await SubscriptionRepo.getCurrentSubscriptionInfo( + userID, + availableSubscriptionInfo.allProducts, ); - currentSubscriptionId = currentSubscriptionInfo.currentSubscriptionId; - currentSubscription = currentSubscriptionInfo.currentSubscription; - allEntitlements = currentSubscriptionInfo.allEntitlements ?? []; - expirationDate = currentSubscriptionInfo.expirationDate; + currentSubscriptionId = rcResponse.currentSubscriptionId; + expirationDate = rcResponse.expirationDate; if (currentSubscriptionId != null && currentSubscription == null) { Sentry.addBreadcrumb( diff --git a/lib/pangea/pages/p_user_age/p_user_age.dart b/lib/pangea/pages/p_user_age/p_user_age.dart index 5fe489485..fd937485c 100644 --- a/lib/pangea/pages/p_user_age/p_user_age.dart +++ b/lib/pangea/pages/p_user_age/p_user_age.dart @@ -91,6 +91,7 @@ class PUserAgeController extends State { return profile; }); } + pangeaController.subscriptionController.reinitialize(); FluffyChatApp.router.go('/rooms'); } catch (err, s) { setState(() { diff --git a/lib/pangea/pages/settings_subscription/settings_subscription.dart b/lib/pangea/pages/settings_subscription/settings_subscription.dart index 374abedb3..5e9b6139c 100644 --- a/lib/pangea/pages/settings_subscription/settings_subscription.dart +++ b/lib/pangea/pages/settings_subscription/settings_subscription.dart @@ -57,30 +57,33 @@ class SubscriptionManagementController extends State { } bool get subscriptionsAvailable => - subscriptionController.subscription?.availableSubscriptions.isNotEmpty ?? + subscriptionController + .availableSubscriptionInfo?.availableSubscriptions.isNotEmpty ?? false; bool get currentSubscriptionAvailable => subscriptionController.isSubscribed && - subscriptionController.subscription?.currentSubscription != null; + subscriptionController.currentSubscriptionInfo?.currentSubscription != + null; - String? get purchasePlatformDisplayName => - subscriptionController.subscription?.purchasePlatformDisplayName; + String? get purchasePlatformDisplayName => subscriptionController + .currentSubscriptionInfo?.purchasePlatformDisplayName; bool get currentSubscriptionIsPromotional => - subscriptionController.subscription?.currentSubscriptionIsPromotional ?? + subscriptionController + .currentSubscriptionInfo?.currentSubscriptionIsPromotional ?? false; bool get isNewUserTrial => - subscriptionController.subscription?.isNewUserTrial ?? false; + subscriptionController.currentSubscriptionInfo?.isNewUserTrial ?? false; String get currentSubscriptionTitle => - subscriptionController.subscription?.currentSubscription + subscriptionController.currentSubscriptionInfo?.currentSubscription ?.displayName(context) ?? ""; String get currentSubscriptionPrice => - subscriptionController.subscription?.currentSubscription + subscriptionController.currentSubscriptionInfo?.currentSubscription ?.displayPrice(context) ?? ""; @@ -88,11 +91,11 @@ class SubscriptionManagementController extends State { if (!currentSubscriptionAvailable || isNewUserTrial) { return false; } - if (subscriptionController.subscription!.purchasedOnWeb) { + if (subscriptionController.currentSubscriptionInfo!.purchasedOnWeb) { return true; } return subscriptionController - .subscription!.currentPlatformMatchesPurchasePlatform; + .currentSubscriptionInfo!.currentPlatformMatchesPurchasePlatform; } void submitChange({bool isPromo = false}) { @@ -122,12 +125,12 @@ class SubscriptionManagementController extends State { if (email != null) { managementUrl += "?prefilled_email=${Uri.encodeComponent(email)}"; } - final String? purchaseAppId = - subscriptionController.subscription?.currentSubscription?.appId; + final String? purchaseAppId = subscriptionController + .currentSubscriptionInfo?.currentSubscription?.appId; if (purchaseAppId == null) return; final SubscriptionAppIds? appIds = - subscriptionController.subscription!.appIds; + subscriptionController.availableSubscriptionInfo!.appIds; if (purchaseAppId == appIds?.stripeId) { launchUrlString(managementUrl); @@ -167,7 +170,7 @@ class SubscriptionManagementController extends State { } bool isCurrentSubscription(SubscriptionDetails subscription) => - subscriptionController.subscription?.currentSubscription == + subscriptionController.currentSubscriptionInfo?.currentSubscription == subscription || isNewUserTrial && subscription.isTrial; diff --git a/lib/pangea/pages/settings_subscription/settings_subscription_view.dart b/lib/pangea/pages/settings_subscription/settings_subscription_view.dart index 9d9f21bf7..c7c89e1d2 100644 --- a/lib/pangea/pages/settings_subscription/settings_subscription_view.dart +++ b/lib/pangea/pages/settings_subscription/settings_subscription_view.dart @@ -51,6 +51,8 @@ class SettingsSubscriptionView extends StatelessWidget { ), ]; + final isSubscribed = controller.subscriptionController.isSubscribed; + return Scaffold( appBar: AppBar( centerTitle: true, @@ -63,13 +65,11 @@ class SettingsSubscriptionView extends StatelessWidget { child: MaxWidthBody( child: Column( children: [ - if (controller.subscriptionController.isSubscribed && - !controller.showManagementOptions) + if (isSubscribed && !controller.showManagementOptions) ManagementNotAvailableWarning( controller: controller, ), - if (!(controller.subscriptionController.isSubscribed) || - controller.isNewUserTrial) + if (!isSubscribed || controller.isNewUserTrial) ChangeSubscription(controller: controller), if (controller.showManagementOptions) ...managementButtons, ], @@ -90,13 +90,14 @@ class ManagementNotAvailableWarning extends StatelessWidget { @override Widget build(BuildContext context) { + final currentSubscriptionInfo = + controller.subscriptionController.currentSubscriptionInfo; + String getWarningText() { final DateFormat formatter = DateFormat('yyyy-MM-dd'); if (controller.isNewUserTrial) { return L10n.of(context)!.trialExpiration( - formatter.format( - controller.subscriptionController.subscription!.expirationDate!, - ), + formatter.format(currentSubscriptionInfo!.expirationDate!), ); } if (controller.currentSubscriptionAvailable) { @@ -108,15 +109,11 @@ class ManagementNotAvailableWarning extends StatelessWidget { return warningText; } if (controller.currentSubscriptionIsPromotional) { - if (controller - .subscriptionController.subscription?.isLifetimeSubscription ?? - false) { + if (currentSubscriptionInfo?.isLifetimeSubscription ?? false) { return L10n.of(context)!.promotionalSubscriptionDesc; } return L10n.of(context)!.promoSubscriptionExpirationDesc( - formatter.format( - controller.subscriptionController.subscription!.expirationDate!, - ), + formatter.format(currentSubscriptionInfo!.expirationDate!), ); } return L10n.of(context)!.subscriptionManagementUnavailable; diff --git a/lib/pangea/repo/subscription_repo.dart b/lib/pangea/repo/subscription_repo.dart index 16ea60c4d..293977921 100644 --- a/lib/pangea/repo/subscription_repo.dart +++ b/lib/pangea/repo/subscription_repo.dart @@ -1,14 +1,13 @@ import 'dart:convert'; -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:http/http.dart' as http; - import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/subscription_app_id.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + import '../network/urls.dart'; class SubscriptionRepo { @@ -120,7 +119,9 @@ class RCProductsResponseModel { .map( (productDetails) => SubscriptionDetails( price: double.parse(metadata['$packageId.price']), - duration: metadata['$packageId.duration'], + duration: SubscriptionDuration.values.firstWhereOrNull( + (duration) => duration.value == metadata['$packageId.duration'], + ), id: productDetails['product']['store_identifier'], appId: productDetails['product']['app_id'], ), @@ -150,9 +151,6 @@ class RCSubscriptionResponseModel { final List activeEntitlements = RCSubscriptionResponseModel.getActiveEntitlements(json); - final List allEntitlements = - RCSubscriptionResponseModel.getAllEntitlements(json); - if (activeEntitlements.length > 1) { debugPrint( "User has more than one active entitlement. This shouldn't happen", diff --git a/lib/pangea/utils/subscription_app_id.dart b/lib/pangea/utils/subscription_app_id.dart index 20a69d4a2..c6de8867c 100644 --- a/lib/pangea/utils/subscription_app_id.dart +++ b/lib/pangea/utils/subscription_app_id.dart @@ -49,15 +49,14 @@ enum RCPlatform { apple, } -class SubscriptionPlatform { - RCPlatform currentPlatform = kIsWeb +extension RCPlatformExtension on RCPlatform { + RCPlatform get currentPlatform => kIsWeb ? RCPlatform.stripe : Platform.isAndroid ? RCPlatform.android : RCPlatform.apple; - @override - String toString() { + String get string { return currentPlatform == RCPlatform.stripe ? 'stripe' : currentPlatform == RCPlatform.android diff --git a/lib/pangea/widgets/chat/message_unsubscribed_card.dart b/lib/pangea/widgets/chat/message_unsubscribed_card.dart index 99a08456e..2e62a558e 100644 --- a/lib/pangea/widgets/chat/message_unsubscribed_card.dart +++ b/lib/pangea/widgets/chat/message_unsubscribed_card.dart @@ -16,7 +16,7 @@ class MessageUnsubscribedCard extends StatelessWidget { @override Widget build(BuildContext context) { final bool inTrialWindow = - MatrixState.pangeaController.userController.inTrialWindow; + MatrixState.pangeaController.userController.inTrialWindow(); return Padding( padding: const EdgeInsets.all(16), diff --git a/lib/pangea/widgets/igc/paywall_card.dart b/lib/pangea/widgets/igc/paywall_card.dart index 20b5fcefe..9308016fb 100644 --- a/lib/pangea/widgets/igc/paywall_card.dart +++ b/lib/pangea/widgets/igc/paywall_card.dart @@ -17,7 +17,7 @@ class PaywallCard extends StatelessWidget { @override Widget build(BuildContext context) { final bool inTrialWindow = - MatrixState.pangeaController.userController.inTrialWindow; + MatrixState.pangeaController.userController.inTrialWindow(); return Column( mainAxisSize: MainAxisSize.max, diff --git a/lib/pangea/widgets/subscription/subscription_buttons.dart b/lib/pangea/widgets/subscription/subscription_buttons.dart index 2af95056a..dd922e216 100644 --- a/lib/pangea/widgets/subscription/subscription_buttons.dart +++ b/lib/pangea/widgets/subscription/subscription_buttons.dart @@ -15,14 +15,16 @@ class SubscriptionButtons extends StatelessWidget { @override Widget build(BuildContext context) { - final bool inTrialWindow = pangeaController.userController.inTrialWindow; + final bool inTrialWindow = pangeaController.userController.inTrialWindow(); return ListView.builder( shrinkWrap: true, - itemCount: controller - .subscriptionController.subscription!.availableSubscriptions.length, + itemCount: controller.subscriptionController.availableSubscriptionInfo! + .availableSubscriptions.length, itemBuilder: (BuildContext context, int i) { final SubscriptionDetails subscription = pangeaController - .subscriptionController.subscription!.availableSubscriptions[i]; + .subscriptionController + .availableSubscriptionInfo! + .availableSubscriptions[i]; return Column( children: [ ListTile( diff --git a/lib/pangea/widgets/subscription/subscription_options.dart b/lib/pangea/widgets/subscription/subscription_options.dart index d40f68023..5584d37fd 100644 --- a/lib/pangea/widgets/subscription/subscription_options.dart +++ b/lib/pangea/widgets/subscription/subscription_options.dart @@ -19,7 +19,7 @@ class SubscriptionOptions extends StatelessWidget { alignment: WrapAlignment.center, direction: Axis.horizontal, spacing: 10, - children: pangeaController.userController.inTrialWindow + children: pangeaController.userController.inTrialWindow() ? [ SubscriptionCard( onTap: () => pangeaController.subscriptionController @@ -27,7 +27,7 @@ class SubscriptionOptions extends StatelessWidget { SubscriptionDetails( price: 0, id: "", - periodType: 'trial', + periodType: SubscriptionPeriodType.trial, ), context, ), @@ -36,8 +36,8 @@ class SubscriptionOptions extends StatelessWidget { buttonText: L10n.of(context)!.activateTrial, ), ] - : pangeaController - .subscriptionController.subscription!.availableSubscriptions + : pangeaController.subscriptionController.availableSubscriptionInfo! + .availableSubscriptions .map( (subscription) => SubscriptionCard( subscription: subscription,