feat: trial using choreo (#2435)

* RC trial

* generated

* fixed mobile

* reverted to resetSubscription

* generated

* fix for unloaded state

* generated

* chore: clean up some unused variables

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>
Co-authored-by: ggurdin <ggurdin@gmail.com>
pull/1817/head
Brord van Wierst 6 months ago committed by GitHub
parent 63ade4c995
commit c0680b5294
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -190,7 +190,6 @@ abstract class AppConfig {
static String assetsBaseURL =
"https://pangea-chat-client-assets.s3.us-east-1.amazonaws.com";
static String trialSubscriptionId = "pangea_new_user_trial";
static String errorSubscriptionId = "pangea_subscription_error";
// Pangea#

@ -57,7 +57,6 @@ class Choreographer {
ChoreoMode choreoMode = ChoreoMode.igc;
final StreamController stateStream = StreamController.broadcast();
StreamSubscription? _trialStream;
StreamSubscription? _languageStream;
late AssistanceState _currentAssistanceState;
@ -72,9 +71,6 @@ class Choreographer {
igc = IgcController(this);
errorService = ErrorService(this);
_textController.addListener(_onChangeListener);
_trialStream = pangeaController
.subscriptionController.trialActivationStream.stream
.listen((_) => _onChangeListener);
_languageStream =
pangeaController.userController.stateStream.listen((update) {
if (update is Map<String, dynamic> &&
@ -568,7 +564,6 @@ class Choreographer {
dispose() {
_textController.dispose();
_trialStream?.cancel();
_languageStream?.cancel();
stateStream.close();
tts.dispose();

@ -22,9 +22,6 @@ class ModelKey {
static const String instructionsSettings = 'instructions_settings';
static const String cefrLevel = 'user_cefr';
// matrix profile keys
// making this a random string so that it's harder to guess
static const String activatedTrialKey = '7C4EuKIsph';
static const String autoPlayMessages = 'autoPlayMessages';
static const String itAutoPlay = 'autoPlayIT';

@ -83,6 +83,7 @@ class PApiUrls {
static String rcAppsChoreo = "${PApiUrls.subscriptionEndpoint}/app_ids";
static String rcProductsChoreo =
"${PApiUrls.subscriptionEndpoint}/all_products";
static String rcProductsTrial = "${PApiUrls.subscriptionEndpoint}/free_trial";
static String rcSubscription = PApiUrls.subscriptionEndpoint;
}

@ -23,6 +23,7 @@ import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/subscription/models/base_subscription_info.dart';
import 'package:fluffychat/pangea/subscription/models/mobile_subscriptions.dart';
import 'package:fluffychat/pangea/subscription/models/web_subscriptions.dart';
import 'package:fluffychat/pangea/subscription/repo/subscription_repo.dart';
import 'package:fluffychat/pangea/subscription/utils/subscription_app_id.dart';
import 'package:fluffychat/pangea/subscription/widgets/subscription_paywall.dart';
import 'package:fluffychat/pangea/user/controllers/user_controller.dart';
@ -42,7 +43,6 @@ class SubscriptionController extends BaseController {
AvailableSubscriptionsInfo? availableSubscriptionInfo;
final StreamController subscriptionStream = StreamController.broadcast();
final StreamController trialActivationStream = StreamController.broadcast();
SubscriptionController(PangeaController pangeaController) : super() {
_pangeaController = pangeaController;
@ -57,11 +57,6 @@ class SubscriptionController extends BaseController {
final bool hasSubscription =
currentSubscriptionInfo?.currentSubscriptionId != null;
if (_activatedNewUserTrial && !hasSubscription) {
_setNewUserTrial();
return true;
}
return hasSubscription;
}
@ -101,20 +96,27 @@ class SubscriptionController extends BaseController {
availableSubscriptionInfo = AvailableSubscriptionsInfo();
await availableSubscriptionInfo!.setAvailableSubscriptions();
final subs =
await SubscriptionRepo.getCurrentSubscriptionInfo(null, null);
currentSubscriptionInfo = kIsWeb
? WebSubscriptionInfo(
userID: _userID!,
availableSubscriptionInfo: availableSubscriptionInfo!,
history: subs.allSubscriptions,
)
: MobileSubscriptionInfo(
userID: _userID!,
availableSubscriptionInfo: availableSubscriptionInfo!,
history: subs.allSubscriptions,
);
await currentSubscriptionInfo!.configure();
await currentSubscriptionInfo!.setCurrentSubscription();
if (_activatedNewUserTrial) {
_setNewUserTrial();
if (currentSubscriptionInfo!.currentSubscriptionId == null &&
_pangeaController.userController.inTrialWindow()) {
await activateNewUserTrial();
}
if (!kIsWeb) {
@ -153,6 +155,25 @@ class SubscriptionController extends BaseController {
),
},
);
if (currentSubscriptionInfo?.currentSubscriptionId == null) {
currentSubscriptionInfo ??= kIsWeb
? WebSubscriptionInfo(
userID: _userID!,
availableSubscriptionInfo:
availableSubscriptionInfo ?? AvailableSubscriptionsInfo(),
history: {},
)
: MobileSubscriptionInfo(
userID: _userID!,
availableSubscriptionInfo:
availableSubscriptionInfo ?? AvailableSubscriptionsInfo(),
history: {},
);
currentSubscriptionInfo!.currentSubscriptionId =
AppConfig.errorSubscriptionId;
}
}
}
@ -163,7 +184,12 @@ class SubscriptionController extends BaseController {
}) async {
if (selectedSubscription != null) {
if (selectedSubscription.isTrial) {
activateNewUserTrial();
try {
await activateNewUserTrial();
await updateCustomerInfo();
} catch (e) {
debugPrint("Failed to initialize trial subscription");
}
return;
}
@ -215,47 +241,17 @@ class SubscriptionController extends BaseController {
}
}
int get _currentTrialDays =>
_userController.inTrialWindow(trialDays: 7) ? 7 : 0;
bool get _activatedNewUserTrial =>
_userController.inTrialWindow(trialDays: 1) ||
(_userController.inTrialWindow() &&
_userController.profile.userSettings.activatedFreeTrial);
void activateNewUserTrial() {
_userController.updateProfile(
(profile) {
profile.userSettings.activatedFreeTrial = true;
return profile;
},
);
_setNewUserTrial();
trialActivationStream.add(true);
}
void _setNewUserTrial() {
final DateTime? createdAt = _userController.profile.userSettings.createdAt;
if (createdAt == null) {
ErrorHandler.logError(
m: "Null user profile createdAt in subscription settings",
s: StackTrace.current,
data: {},
);
return;
Future<void> activateNewUserTrial() async {
if (await SubscriptionRepo.activateFreeTrial()) {
await updateCustomerInfo();
}
final DateTime expirationDate = createdAt.add(
Duration(days: _currentTrialDays),
);
currentSubscriptionInfo?.setTrial(expirationDate);
}
Future<void> updateCustomerInfo() async {
if (!initCompleter.isCompleted) {
await initialize();
}
await currentSubscriptionInfo!.setCurrentSubscription();
await currentSubscriptionInfo?.setCurrentSubscription();
setState(null);
}
@ -384,11 +380,6 @@ class SubscriptionController extends BaseController {
?.defaultManagementURL(availableSubscriptionInfo?.appIds);
}
enum SubscriptionPeriodType {
normal,
trial,
}
enum SubscriptionDuration {
month,
year,
@ -403,7 +394,6 @@ class SubscriptionDetails {
final SubscriptionDuration? duration;
final String? appId;
final String id;
SubscriptionPeriodType periodType;
Package? package;
String? localizedPrice;
@ -413,11 +403,9 @@ class SubscriptionDetails {
this.duration,
this.package,
this.appId,
this.periodType = SubscriptionPeriodType.normal,
});
void makeTrial() => periodType = SubscriptionPeriodType.trial;
bool get isTrial => periodType == SubscriptionPeriodType.trial;
bool get isTrial => appId == "trial";
String displayPrice(BuildContext context) => isTrial || price <= 0
? L10n.of(context).freeTrial

@ -1,6 +1,5 @@
import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/constants/local.key.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
@ -12,6 +11,7 @@ import 'package:fluffychat/widgets/matrix.dart';
class CurrentSubscriptionInfo {
final String userID;
final AvailableSubscriptionsInfo availableSubscriptionInfo;
final Map<String, RCSubscription>? history;
DateTime? expirationDate;
String? currentSubscriptionId;
@ -19,6 +19,7 @@ class CurrentSubscriptionInfo {
CurrentSubscriptionInfo({
required this.userID,
required this.availableSubscriptionInfo,
required this.history,
});
SubscriptionDetails? get currentSubscription {
@ -32,15 +33,11 @@ class CurrentSubscriptionInfo {
Future<void> configure() async {}
bool get isNewUserTrial =>
currentSubscriptionId == AppConfig.trialSubscriptionId;
bool get currentSubscriptionIsPromotional =>
currentSubscriptionId?.startsWith("rc_promo") ?? false;
bool get isPaidSubscription =>
(currentSubscription != null || currentSubscriptionId != null) &&
!isNewUserTrial &&
!currentSubscriptionIsPromotional;
bool get isLifetimeSubscription =>
@ -67,23 +64,6 @@ class CurrentSubscriptionInfo {
availableSubscriptionInfo.appIds?.currentAppId);
void resetSubscription() => currentSubscriptionId = null;
void setTrial(DateTime expiration) {
expirationDate = expiration;
currentSubscriptionId = AppConfig.trialSubscriptionId;
if (currentSubscription == null &&
!availableSubscriptionInfo.availableSubscriptions
.any((sub) => sub.isTrial)) {
availableSubscriptionInfo.availableSubscriptions.add(
SubscriptionDetails(
price: 0,
id: AppConfig.trialSubscriptionId,
periodType: SubscriptionPeriodType.trial,
),
);
}
}
Future<void> setCurrentSubscription() async {}
}
@ -109,7 +89,10 @@ class AvailableSubscriptionsInfo {
if (cachedInfo == null) await _cacheSubscriptionInfo();
availableSubscriptions = (allProducts ?? [])
.where((product) => product.appId == appIds!.currentAppId)
.where(
(product) =>
product.appId == appIds!.currentAppId || product.appId == "trial",
)
.sorted((a, b) => a.price.compareTo(b.price))
.toList();
}

@ -15,6 +15,7 @@ class MobileSubscriptionInfo extends CurrentSubscriptionInfo {
MobileSubscriptionInfo({
required super.userID,
required super.availableSubscriptionInfo,
required super.history,
});
@override
@ -65,7 +66,7 @@ class MobileSubscriptionInfo extends CurrentSubscriptionInfo {
CustomerInfo info;
try {
// await Purchases.syncPurchases();
await Purchases.invalidateCustomerInfoCache();
info = await Purchases.getCustomerInfo();
} catch (err) {
ErrorHandler.logError(
@ -81,9 +82,10 @@ class MobileSubscriptionInfo extends CurrentSubscriptionInfo {
info.entitlements.all.entries
.where(
(MapEntry<String, EntitlementInfo> entry) =>
entry.value.expirationDate == null ||
DateTime.parse(entry.value.expirationDate!)
.isAfter(DateTime.now()),
entry.value.isActive &&
(entry.value.expirationDate == null ||
DateTime.parse(entry.value.expirationDate!)
.isAfter(DateTime.now())),
)
.map((MapEntry<String, EntitlementInfo> entry) => entry.value)
.toList();
@ -94,9 +96,7 @@ class MobileSubscriptionInfo extends CurrentSubscriptionInfo {
);
} else if (activeEntitlements.isEmpty) {
debugPrint("User has no active entitlements");
if (!isNewUserTrial) {
resetSubscription();
}
resetSubscription();
return;
}
@ -107,7 +107,7 @@ class MobileSubscriptionInfo extends CurrentSubscriptionInfo {
: null;
if (activeEntitlement.periodType == PeriodType.trial) {
currentSubscription?.makeTrial();
// We dont use actual trials as it would require adding a CC on devices
}
if (currentSubscriptionId != null && currentSubscription == null) {
Sentry.addBreadcrumb(

@ -8,6 +8,7 @@ class WebSubscriptionInfo extends CurrentSubscriptionInfo {
WebSubscriptionInfo({
required super.userID,
required super.availableSubscriptionInfo,
required super.history,
});
@override

@ -71,6 +71,12 @@ class SubscriptionManagementController extends State<SubscriptionManagement> {
subscriptionController.currentSubscriptionInfo?.currentSubscription !=
null;
bool get currentSubscriptionIsTrial =>
currentSubscriptionAvailable &&
(subscriptionController
.currentSubscriptionInfo?.currentSubscription?.isTrial ??
false);
String? get purchasePlatformDisplayName => subscriptionController
.currentSubscriptionInfo?.purchasePlatformDisplayName;
@ -79,9 +85,6 @@ class SubscriptionManagementController extends State<SubscriptionManagement> {
.currentSubscriptionInfo?.currentSubscriptionIsPromotional ??
false;
bool get isNewUserTrial =>
subscriptionController.currentSubscriptionInfo?.isNewUserTrial ?? false;
String get currentSubscriptionTitle =>
subscriptionController.currentSubscriptionInfo?.currentSubscription
?.displayName(context) ??
@ -93,7 +96,7 @@ class SubscriptionManagementController extends State<SubscriptionManagement> {
"";
bool get showManagementOptions {
if (!currentSubscriptionAvailable || isNewUserTrial) {
if (!currentSubscriptionAvailable) {
return false;
}
if (subscriptionController.currentSubscriptionInfo!.purchasedOnWeb) {
@ -183,8 +186,7 @@ class SubscriptionManagementController extends State<SubscriptionManagement> {
bool isCurrentSubscription(SubscriptionDetails subscription) =>
subscriptionController.currentSubscriptionInfo?.currentSubscription ==
subscription ||
isNewUserTrial && subscription.isTrial;
subscription;
@override
Widget build(BuildContext context) => SettingsSubscriptionView(this);

@ -75,8 +75,7 @@ class SettingsSubscriptionView extends StatelessWidget {
ManagementNotAvailableWarning(
controller: controller,
),
if (isSubscribed != null && !isSubscribed ||
controller.isNewUserTrial)
if (isSubscribed != null && !isSubscribed)
ChangeSubscription(controller: controller),
if (controller.showManagementOptions) ...managementButtons,
],
@ -102,11 +101,12 @@ class ManagementNotAvailableWarning extends StatelessWidget {
String getWarningText() {
final DateFormat formatter = DateFormat('yyyy-MM-dd');
if (controller.isNewUserTrial) {
if (controller.currentSubscriptionIsTrial) {
return L10n.of(context).trialExpiration(
formatter.format(currentSubscriptionInfo!.expirationDate!),
);
}
if (controller.currentSubscriptionAvailable) {
String warningText = L10n.of(context).subsciptionPlatformTooltip;
if (controller.purchasePlatformDisplayName != null) {
@ -115,6 +115,7 @@ class ManagementNotAvailableWarning extends StatelessWidget {
}
return warningText;
}
if (controller.currentSubscriptionIsPromotional) {
if (currentSubscriptionInfo?.isLifetimeSubscription ?? false) {
return L10n.of(context).promotionalSubscriptionDesc;
@ -123,6 +124,7 @@ class ManagementNotAvailableWarning extends StatelessWidget {
formatter.format(currentSubscriptionInfo!.expirationDate!),
);
}
return L10n.of(context).subscriptionManagementUnavailable;
}

@ -61,6 +61,35 @@ class SubscriptionRepo {
}
}
static Future<bool> activateFreeTrial() async {
try {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final http.Response res = await req.get(
url: PApiUrls.rcProductsTrial,
);
if (res.statusCode != 201) {
ErrorHandler.logError(
e: res.body,
data: {},
);
return false;
} else {
return true;
}
} catch (err, s) {
ErrorHandler.logError(
e: err,
s: s,
data: {},
);
return false;
}
}
static Future<RCSubscriptionResponseModel> getCurrentSubscriptionInfo(
String? userId,
List<SubscriptionDetails>? allProducts,
@ -125,12 +154,14 @@ class RCSubscriptionResponseModel {
SubscriptionDetails? currentSubscription;
DateTime? expirationDate;
List<String>? allEntitlements;
Map<String, RCSubscription>? allSubscriptions;
RCSubscriptionResponseModel({
this.currentSubscriptionId,
this.currentSubscription,
this.allEntitlements,
this.expirationDate,
this.allSubscriptions,
});
factory RCSubscriptionResponseModel.fromJson(
@ -145,9 +176,16 @@ class RCSubscriptionResponseModel {
"User has more than one active entitlement. This shouldn't happen",
);
}
final history = (json['subscriptions'] as Map<String, dynamic>).map(
(key, value) => MapEntry(key, RCSubscription.fromJson(value)),
);
if (activeEntitlements.isEmpty) {
debugPrint("User has no active entitlements");
return RCSubscriptionResponseModel();
return RCSubscriptionResponseModel(
allSubscriptions: history,
);
}
final String currentSubscriptionId = activeEntitlements[0];
@ -159,9 +197,6 @@ class RCSubscriptionResponseModel {
currentSubscriptionMetadata['expires_date'],
);
final String currentSubscriptionPeriodType =
currentSubscriptionMetadata['period_type'] ?? "";
final SubscriptionDetails? currentSubscription =
allProducts?.firstWhereOrNull(
(SubscriptionDetails sub) =>
@ -169,15 +204,12 @@ class RCSubscriptionResponseModel {
currentSubscriptionId.contains(sub.id),
);
if (currentSubscriptionPeriodType == "trial") {
currentSubscription?.makeTrial();
}
return RCSubscriptionResponseModel(
currentSubscription: currentSubscription,
currentSubscriptionId: currentSubscriptionId,
expirationDate: expirationDate,
allEntitlements: activeEntitlements,
allSubscriptions: history,
);
}
@ -208,3 +240,50 @@ class RCSubscriptionResponseModel {
.toList();
}
}
class RCSubscription {
final String? autoResumeDate;
final String? billingIssuesDetectedAt;
final String expiresDate;
final String? gracePeriodExpiresDate;
final bool isSandbox;
final String originalPurchaseDate;
final String periodType;
final String purchaseDate;
final String? refundedAt;
final String store;
final String storeTransactionId;
final String? unsubscribeDetectedAt;
RCSubscription({
required this.autoResumeDate,
required this.billingIssuesDetectedAt,
required this.expiresDate,
required this.gracePeriodExpiresDate,
required this.isSandbox,
required this.originalPurchaseDate,
required this.periodType,
required this.purchaseDate,
required this.refundedAt,
required this.store,
required this.storeTransactionId,
required this.unsubscribeDetectedAt,
});
factory RCSubscription.fromJson(Map<String, dynamic> json) {
return RCSubscription(
autoResumeDate: json['auto_resume_date'],
billingIssuesDetectedAt: json['billing_issues_detected_at'],
expiresDate: json['expires_date'],
gracePeriodExpiresDate: json['grace_period_expires_date'],
isSandbox: json['is_sandbox'],
originalPurchaseDate: json['original_purchase_date'],
periodType: json['period_type'],
purchaseDate: json['purchase_date'],
refundedAt: json['refunded_at'],
store: json['store'],
storeTransactionId: json['store_transaction_id'],
unsubscribeDetectedAt: json['unsubscribe_detected_at'],
);
}
}

@ -25,14 +25,7 @@ class SubscriptionOptions extends StatelessWidget {
? [
SubscriptionCard(
onTap: () => pangeaController.subscriptionController
.submitSubscriptionChange(
SubscriptionDetails(
price: 0,
id: "",
periodType: SubscriptionPeriodType.trial,
),
context,
),
.activateNewUserTrial(),
title: L10n.of(context).freeTrial,
description: L10n.of(context).freeTrialDesc,
buttonText: L10n.of(context).activateTrial,

@ -273,6 +273,7 @@ class UserController extends BaseController {
if (createdAt == null) {
return false;
}
return createdAt.isAfter(
DateTime.now().subtract(Duration(days: trialDays)),
);

@ -13,8 +13,6 @@ class UserSettings {
DateTime? dateOfBirth;
DateTime? createdAt;
bool? autoPlayMessages;
// bool itAutoPlay;
bool activatedFreeTrial;
bool? publicProfile;
String? targetLanguage;
String? sourceLanguage;
@ -26,8 +24,6 @@ class UserSettings {
this.dateOfBirth,
this.createdAt,
this.autoPlayMessages,
// this.itAutoPlay = true,
this.activatedFreeTrial = false,
this.publicProfile,
this.targetLanguage,
this.sourceLanguage,
@ -44,8 +40,6 @@ class UserSettings {
? DateTime.parse(json[ModelKey.userCreatedAt])
: null,
autoPlayMessages: json[ModelKey.autoPlayMessages],
// itAutoPlay: json[ModelKey.itAutoPlay] ?? true,
activatedFreeTrial: json[ModelKey.activatedTrialKey] ?? false,
publicProfile: json[ModelKey.publicProfile],
targetLanguage: json[ModelKey.l2LanguageKey],
sourceLanguage: json[ModelKey.l1LanguageKey],
@ -63,8 +57,6 @@ class UserSettings {
data[ModelKey.userDateOfBirth] = dateOfBirth?.toIso8601String();
data[ModelKey.userCreatedAt] = createdAt?.toIso8601String();
data[ModelKey.autoPlayMessages] = autoPlayMessages;
// data[ModelKey.itAutoPlay] = itAutoPlay;
data[ModelKey.activatedTrialKey] = activatedFreeTrial;
data[ModelKey.publicProfile] = publicProfile;
data[ModelKey.l2LanguageKey] = targetLanguage;
data[ModelKey.l1LanguageKey] = sourceLanguage;
@ -111,12 +103,6 @@ class UserSettings {
autoPlayMessages: (accountData[ModelKey.autoPlayMessages]
?.content[ModelKey.autoPlayMessages] as bool?) ??
false,
// itAutoPlay: (accountData[ModelKey.itAutoPlay]
// ?.content[ModelKey.itAutoPlay] as bool?) ??
// true,
activatedFreeTrial: (accountData[ModelKey.activatedTrialKey]
?.content[ModelKey.activatedTrialKey] as bool?) ??
false,
publicProfile: (accountData[ModelKey.publicProfile]
?.content[ModelKey.publicProfile] as bool?) ??
false,
@ -134,7 +120,6 @@ class UserSettings {
dateOfBirth: dateOfBirth,
createdAt: createdAt,
autoPlayMessages: autoPlayMessages,
activatedFreeTrial: activatedFreeTrial,
publicProfile: publicProfile,
targetLanguage: targetLanguage,
sourceLanguage: sourceLanguage,

Loading…
Cancel
Save