refactor: move local cache of subscription info into its own repo, dismiss paywall on show initial paywall card (#4532)

This commit is contained in:
ggurdin 2025-10-28 10:51:35 -04:00 committed by GitHub
parent a2a81733bd
commit c67dc2ab18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 120 additions and 143 deletions

View file

@ -42,7 +42,11 @@ void main() async {
/// Then where ever you need language functions simply call PangeaLanguage pangeaLanguage = PangeaLanguage()
/// pangeaLanguage.getList or whatever function you need
///
await GetStorage.init();
final List<Future> initFutures = [
GetStorage.init(),
GetStorage.init("subscription_storage"),
];
await Future.wait(initFutures);
// Pangea#
// Our background push shared isolate accesses flutter-internal things very early in the startup proccess

View file

@ -19,7 +19,6 @@ import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
@ -107,15 +106,7 @@ class Choreographer {
// show the paywall if applicable or just send the message
final status = pangeaController.subscriptionController.subscriptionStatus;
status == SubscriptionStatus.shouldShowPaywall
? OverlayUtil.showPositionedCard(
context: context,
cardToShow: PaywallCard(
chatController: chatController,
),
maxHeight: 325,
maxWidth: 325,
transformTargetId: inputTransformTargetKey,
)
? PaywallCard.show(context, chatController)
: chatController.send(
message: chatController.sendController.text,
);

View file

@ -50,15 +50,8 @@ class PangeaTextController extends TextEditingController {
SubscriptionStatus.shouldShowPaywall &&
!choreographer.isFetching &&
text.isNotEmpty) {
OverlayUtil.showPositionedCard(
context: context,
cardToShow: PaywallCard(
chatController: choreographer.chatController,
),
maxHeight: 325,
maxWidth: 325,
transformTargetId: choreographer.inputTransformTargetKey,
);
PaywallCard.show(context, choreographer.chatController);
return;
}
// if there is no igc text data, then don't do anything

View file

@ -5,6 +5,8 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_header.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/subscription/repo/subscription_management_repo.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PaywallCard extends StatelessWidget {
@ -14,6 +16,27 @@ class PaywallCard extends StatelessWidget {
required this.chatController,
});
static Future<void> show(
BuildContext context,
ChatController chatController,
) async {
if (!MatrixState
.pangeaController.subscriptionController.shouldShowPaywall) {
return;
}
await SubscriptionManagementRepo.setDismissedPaywall();
OverlayUtil.showPositionedCard(
context: context,
cardToShow: PaywallCard(
chatController: chatController,
),
maxHeight: 325,
maxWidth: 325,
transformTargetId: chatController.choreographer.inputTransformTargetKey,
);
}
@override
Widget build(BuildContext context) {
final bool inTrialWindow =
@ -26,10 +49,6 @@ class PaywallCard extends StatelessWidget {
CardHeader(
text: L10n.of(context).clickMessageTitle,
botExpression: BotExpression.addled,
onClose: () {
MatrixState.pangeaController.subscriptionController
.dismissPaywall();
},
),
Padding(
padding: const EdgeInsets.all(8),

View file

@ -7,7 +7,6 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/choreographer/enums/assistance_state_enum.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
import 'package:fluffychat/pangea/common/utils/overlay.dart';
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
import '../../../pages/chat/chat.dart';
@ -81,16 +80,7 @@ class StartIGCButtonState extends State<StartIGCButton>
Future<void> _onTap() async {
switch (assistanceState) {
case AssistanceState.noSub:
OverlayUtil.showPositionedCard(
context: context,
cardToShow: PaywallCard(
chatController: widget.controller,
),
maxHeight: 325,
maxWidth: 325,
transformTargetId:
widget.controller.choreographer.inputTransformTargetKey,
);
await PaywallCard.show(context, widget.controller);
return;
case AssistanceState.noMessage:
showDialog(

View file

@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -13,7 +12,6 @@ import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/constants/local.key.dart';
import 'package:fluffychat/pangea/common/controllers/base_controller.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
@ -23,6 +21,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_management_repo.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';
@ -37,7 +36,6 @@ enum SubscriptionStatus {
}
class SubscriptionController extends BaseController {
static final GetStorage subscriptionBox = GetStorage("subscription_storage");
late PangeaController _pangeaController;
CurrentSubscriptionInfo? currentSubscriptionInfo;
@ -129,12 +127,8 @@ class SubscriptionController extends BaseController {
},
);
} else {
final bool? beganWebPayment =
subscriptionBox.read(PLocalKey.beganWebPayment);
if (beganWebPayment ?? false) {
await subscriptionBox.remove(
PLocalKey.beganWebPayment,
);
if (SubscriptionManagementRepo.getBeganWebPayment()) {
await SubscriptionManagementRepo.removeBeganWebPayment();
if (isSubscribed != null && isSubscribed!) {
subscriptionStream.add(true);
}
@ -202,10 +196,7 @@ class SubscriptionController extends BaseController {
selectedSubscription.duration!,
isPromo: isPromo,
);
await subscriptionBox.write(
PLocalKey.beganWebPayment,
true,
);
await SubscriptionManagementRepo.setBeganWebPayment();
setState(null);
launchUrlString(
paymentLink,
@ -255,54 +246,16 @@ class SubscriptionController extends BaseController {
return isSubscribed!
? SubscriptionStatus.subscribed
: _shouldShowPaywall
: shouldShowPaywall
? SubscriptionStatus.shouldShowPaywall
: SubscriptionStatus.dimissedPaywall;
}
DateTime? get _lastDismissedPaywall {
final lastDismissed = subscriptionBox.read(
PLocalKey.dismissedPaywall,
);
if (lastDismissed == null) return null;
return DateTime.tryParse(lastDismissed);
}
int? get _paywallBackoff {
final backoff = subscriptionBox.read(
PLocalKey.paywallBackoff,
);
if (backoff == null) return null;
return backoff;
}
/// whether or not the paywall should be shown
bool get _shouldShowPaywall {
bool get shouldShowPaywall {
return initCompleter.isCompleted &&
isSubscribed != null &&
!isSubscribed! &&
(_lastDismissedPaywall == null ||
DateTime.now().difference(_lastDismissedPaywall!).inHours >
(1 * (_paywallBackoff ?? 1)));
}
void dismissPaywall() async {
await subscriptionBox.write(
PLocalKey.dismissedPaywall,
DateTime.now().toString(),
);
if (_paywallBackoff == null) {
await subscriptionBox.write(
PLocalKey.paywallBackoff,
1,
);
} else {
await subscriptionBox.write(
PLocalKey.paywallBackoff,
_paywallBackoff! + 1,
);
}
isSubscribed == false &&
!SubscriptionManagementRepo.getDismissedPaywall();
}
Future<void> showPaywall(BuildContext context) async {
@ -330,7 +283,7 @@ class SubscriptionController extends BaseController {
);
},
);
dismissPaywall();
await SubscriptionManagementRepo.setDismissedPaywall();
} catch (e, s) {
ErrorHandler.logError(
e: e,
@ -369,37 +322,6 @@ class SubscriptionController extends BaseController {
String? get defaultManagementURL =>
currentSubscriptionInfo?.currentSubscription
?.defaultManagementURL(availableSubscriptionInfo?.appIds);
Future<void> setCachedSubscriptionInfo(
AvailableSubscriptionsInfo info,
) =>
subscriptionBox.write(
PLocalKey.availableSubscriptionInfo,
info.toJson(),
);
Future<AvailableSubscriptionsInfo?> getCachedSubscriptionInfo() async {
final entry = subscriptionBox.read(
PLocalKey.availableSubscriptionInfo,
);
if (entry is! Map<String, dynamic>) {
return null;
}
try {
final resp = AvailableSubscriptionsInfo.fromJson(entry);
return resp.lastUpdated == null ? null : resp;
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"entry": entry,
},
);
return null;
}
}
}
enum SubscriptionDuration {

View file

@ -1,10 +1,9 @@
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/subscription/repo/subscription_management_repo.dart';
import 'package:fluffychat/pangea/subscription/repo/subscription_repo.dart';
import 'package:fluffychat/pangea/subscription/utils/subscription_app_id.dart';
import 'package:fluffychat/widgets/matrix.dart';
/// Contains information about the users's current subscription
class CurrentSubscriptionInfo {
@ -78,13 +77,16 @@ class AvailableSubscriptionsInfo {
});
Future<void> setAvailableSubscriptions() async {
final cachedInfo = await MatrixState.pangeaController.subscriptionController
.getCachedSubscriptionInfo();
final cachedInfo =
SubscriptionManagementRepo.getAvailableSubscriptionsInfo();
appIds ??= cachedInfo?.appIds ?? await SubscriptionRepo.getAppIds();
allProducts ??=
cachedInfo?.allProducts ?? await SubscriptionRepo.getAllProducts();
if (cachedInfo == null) await _cacheSubscriptionInfo();
if (cachedInfo == null) {
await SubscriptionManagementRepo.setAvailableSubscriptionsInfo(this);
}
availableSubscriptions = (allProducts ?? [])
.where(
@ -95,22 +97,6 @@ class AvailableSubscriptionsInfo {
.toList();
}
Future<void> _cacheSubscriptionInfo() async {
try {
MatrixState.pangeaController.subscriptionController
.setCachedSubscriptionInfo(this);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
"appIds": appIds,
"allProducts": allProducts,
},
);
}
}
factory AvailableSubscriptionsInfo.fromJson(Map<String, dynamic> json) {
if (!json.containsKey('app_ids') || !json.containsKey('all_products')) {
throw "Cached subscription info is missing required fields";

View file

@ -0,0 +1,72 @@
import 'package:get_storage/get_storage.dart';
import 'package:fluffychat/pangea/common/constants/local.key.dart';
import 'package:fluffychat/pangea/subscription/models/base_subscription_info.dart';
class SubscriptionManagementRepo {
static final GetStorage _cache = GetStorage("subscription_storage");
static AvailableSubscriptionsInfo? getAvailableSubscriptionsInfo() {
final entry = _cache.read(PLocalKey.availableSubscriptionInfo);
if (entry == null) return null;
try {
return AvailableSubscriptionsInfo.fromJson(entry);
} catch (e) {
_cache.remove(PLocalKey.availableSubscriptionInfo);
return null;
}
}
static Future<void> setAvailableSubscriptionsInfo(
AvailableSubscriptionsInfo info,
) async {
await _cache.write(
PLocalKey.availableSubscriptionInfo,
info.toJson(),
);
}
static bool getBeganWebPayment() {
return _cache.read(PLocalKey.beganWebPayment) ?? false;
}
static Future<void> setBeganWebPayment() async {
await _cache.write(PLocalKey.beganWebPayment, true);
}
static Future<void> removeBeganWebPayment() async {
await _cache.remove(PLocalKey.beganWebPayment);
}
static bool getDismissedPaywall() {
final entry = _cache.read(PLocalKey.dismissedPaywall);
if (entry == null) return false;
try {
final dismissed = DateTime.parse(entry);
final nextValidShowtime = dismissed.add(
Duration(hours: 1 * _getPaywallBackoff()),
);
return DateTime.now().isBefore(nextValidShowtime);
} catch (e) {
_cache.remove(PLocalKey.dismissedPaywall);
return false;
}
}
static Future<void> setDismissedPaywall() async {
await _cache.write(
PLocalKey.dismissedPaywall,
DateTime.now().toIso8601String(),
);
await _incrementPaywallBackoff();
}
static int _getPaywallBackoff() {
return _cache.read(PLocalKey.paywallBackoff) ?? 0;
}
static Future<void> _incrementPaywallBackoff() async {
final int backoff = _getPaywallBackoff() + 1;
await _cache.write(PLocalKey.paywallBackoff, backoff);
}
}