some refactoring to subscriptions, added auto 1-day pretrial

This commit is contained in:
ggurdin 2024-10-29 15:20:55 -04:00
parent c8865b12a4
commit d0caf01e4d
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
15 changed files with 251 additions and 388 deletions

View file

@ -54,8 +54,9 @@ class SettingsSecurityController extends State<SettingsSecurity> {
// #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<SettingsSecurity> {
);
if (resp == OkCancelResult.ok) {
launchUrlString(
subscriptionController.subscription!.defaultManagementURL!,
subscriptionController.defaultManagementURL!,
mode: LaunchMode.externalApplication,
);
return;

View file

@ -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<void> initialized = Completer<void>();
@ -67,18 +73,27 @@ class SubscriptionController extends BaseController {
Future<void> _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<void> 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<String> getPaymentLink(String duration, {bool isPromo = false}) async {
Future<String> 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<bool> 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<void> redeemPromoCode(BuildContext context) async {
final List<String>? 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;
}
}

View file

@ -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)),
);
}

View file

@ -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<SubscriptionDetails> availableSubscriptions = [];
String? currentSubscriptionId;
SubscriptionDetails? currentSubscription;
// Gabby - is it necessary to store appIds for each platform?
SubscriptionAppIds? appIds;
List<SubscriptionDetails>? allProducts;
final SubscriptionPlatform platform = SubscriptionPlatform();
List<String> 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<void> 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<void> setAppIds() async {
if (appIds != null) return;
appIds = await SubscriptionRepo.getAppIds();
}
Future<void> 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<void> setCustomerInfo() async {}
Future<void> 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<SubscriptionDetails> availableSubscriptions = [];
SubscriptionAppIds? appIds;
List<SubscriptionDetails>? allProducts;
Future<void> 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,
// ),
// );
// }
}
}

View file

@ -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<void> 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<void> 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<SubscriptionDetails> mobileSubscriptions =
offering.availablePackages
.map(
(package) {
return SubscriptionDetails(
price: package.storeProduct.price,
id: package.storeProduct.identifier,
package: package,
);
},
)
.toList()
.cast<SubscriptionDetails>();
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<void> setCustomerInfo() async {
if (allProducts == null) {
ErrorHandler.logError(
m: "Null allProducts in setCustomerInfo",
s: StackTrace.current,
);
debugPrint(
"Null allProducts in setCustomerInfo",
);
return;
}
Future<void> 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<EntitlementInfo> 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<EntitlementInfo> activeEntitlements =
info.entitlements.all.entries
@ -166,14 +81,6 @@ class MobileSubscriptionInfo extends SubscriptionInfo {
.map((MapEntry<String, EntitlementInfo> entry) => entry.value)
.toList();
allEntitlements = info.entitlements.all.entries
.map(
(MapEntry<String, EntitlementInfo> entry) =>
entry.value.productIdentifier,
)
.cast<String>()
.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<EntitlementInfo> getEntitlementsWithoutExpiration(CustomerInfo info) {
final List<EntitlementInfo> noExpirations = info.entitlements.all.entries
.where(
(MapEntry<String, EntitlementInfo> entry) =>
entry.value.expirationDate == null,
)
.map((MapEntry<String, EntitlementInfo> entry) => entry.value)
.toList();
return noExpirations;
}
}

View file

@ -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<void> 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<void> setCustomerInfo() async {
if (currentSubscriptionId != null && currentSubscription != null) {
return;
}
final RCSubscriptionResponseModel currentSubscriptionInfo =
await SubscriptionRepo.getCurrentSubscriptionInfo(
pangeaController.matrixState.client.userID,
allProducts,
Future<void> 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(

View file

@ -91,6 +91,7 @@ class PUserAgeController extends State<PUserAge> {
return profile;
});
}
pangeaController.subscriptionController.reinitialize();
FluffyChatApp.router.go('/rooms');
} catch (err, s) {
setState(() {

View file

@ -57,30 +57,33 @@ class SubscriptionManagementController extends State<SubscriptionManagement> {
}
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<SubscriptionManagement> {
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<SubscriptionManagement> {
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<SubscriptionManagement> {
}
bool isCurrentSubscription(SubscriptionDetails subscription) =>
subscriptionController.subscription?.currentSubscription ==
subscriptionController.currentSubscriptionInfo?.currentSubscription ==
subscription ||
isNewUserTrial && subscription.isTrial;

View file

@ -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;

View file

@ -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<String> activeEntitlements =
RCSubscriptionResponseModel.getActiveEntitlements(json);
final List<String> allEntitlements =
RCSubscriptionResponseModel.getAllEntitlements(json);
if (activeEntitlements.length > 1) {
debugPrint(
"User has more than one active entitlement. This shouldn't happen",

View file

@ -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

View file

@ -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),

View file

@ -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,

View file

@ -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(

View file

@ -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,