updates to subscription paywall flow

pull/1011/head
Gabby Gurdin 2 years ago
parent 8876c86280
commit fb07dc747a

@ -3927,5 +3927,10 @@
"placeholders": {
"sender": {}
}
}
},
"subscriptionPopupTitle": "This sentence could have a grammar mistake...",
"subscriptionPopupDesc": "Subscribe today to unlock translation and grammar correction!",
"seeOptions": "See options",
"continuedWithoutSubscription": "Continue without subscribing",
"trialPeriodExpired": "Your trial period has expired"
}

@ -1,11 +1,6 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart';
import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart';
@ -13,6 +8,7 @@ import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/enum/edit_type.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/class_model.dart';
@ -21,6 +17,12 @@ import 'package:fluffychat/pangea/models/message_data_models.dart';
import 'package:fluffychat/pangea/models/widget_measurement.dart';
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/overlay.dart';
import 'package:fluffychat/pangea/widgets/igc/paywall_card.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../../widgets/matrix.dart';
import '../../enum/use_type.dart';
import '../../models/choreo_record.dart';
@ -51,8 +53,6 @@ class Choreographer {
ChoreoMode choreoMode = ChoreoMode.igc;
final StreamController stateListener = StreamController();
bool toldToPay = false;
Choreographer(this.pangeaController, this.chatController) {
_initialize();
}
@ -71,9 +71,14 @@ class Choreographer {
void send(BuildContext context) {
if (isFetching) return;
if (!pangeaController.subscriptionController.isSubscribed && !toldToPay) {
toldToPay = true;
pangeaController.subscriptionController.showPaywall(context);
if (pangeaController.subscriptionController.canSendStatus ==
CanSendStatus.showPaywall) {
OverlayUtil.showPositionedCard(
context: context,
cardToShow: const PaywallCard(),
cardSize: const Size(325, 375),
transformTargetId: inputTransformTargetKey,
);
return;
}
@ -199,22 +204,20 @@ class Choreographer {
Future<void> getLanguageHelp([bool tokensOnly = false]) async {
try {
if (errorService.isError) return;
if (!pangeaController.subscriptionController.isSubscribed &&
pangeaController.subscriptionController.initialized) {
debugPrint('setting not subscribed error');
errorService.setErrorAndLock(
ChoreoError(
type: ChoreoErrorType.unsubscribed,
),
);
final CanSendStatus canSendStatus =
pangeaController.subscriptionController.canSendStatus;
if (canSendStatus != CanSendStatus.subscribed) {
return;
}
startLoading();
if (choreoMode == ChoreoMode.it &&
itController.isTranslationDone &&
!tokensOnly) {
debugger(when: kDebugMode);
}
await (choreoMode == ChoreoMode.it && !itController.isTranslationDone
? itController.getTranslationData(_useCustomInput)
: igc.getIGCTextData(tokensOnly: tokensOnly));

@ -1,13 +1,12 @@
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import '../../utils/error_handler.dart';
enum ChoreoErrorType {
unknown,
classDisabled,
userDisabled,
unsubscribed,
}
class ChoreoError {
@ -22,8 +21,6 @@ class ChoreoError {
return "Class Disabled";
case ChoreoErrorType.userDisabled:
return "User Disabled";
case ChoreoErrorType.unsubscribed:
return "Unsubscribed";
default:
return ErrorCopy(context, raw).title;
}
@ -35,8 +32,6 @@ class ChoreoError {
return "Class Disabled";
case ChoreoErrorType.userDisabled:
return "User Disabled";
case ChoreoErrorType.unsubscribed:
return "Unsubscribed";
default:
return ErrorCopy(context, raw).body;
}
@ -48,8 +43,6 @@ class ChoreoError {
return Icons.history_edu_outlined;
case ChoreoErrorType.userDisabled:
return Icons.history_edu_outlined;
case ChoreoErrorType.unsubscribed:
return Icons.lock_outline;
default:
return Icons.error_outline;
}

@ -28,8 +28,6 @@ class ChoreographerHasErrorButton extends StatelessWidget {
),
),
);
} else if (error.type == ChoreoErrorType.unsubscribed) {
pangeaController.subscriptionController.showPaywall(context);
}
},
mini: true,

@ -8,4 +8,6 @@ class PLocalKey {
// making this a random string so that it's harder to guess
static const String activatedTrialKey = '7C4EuKIsph';
static const String dismissedPaywall = 'dismissedPaywall';
static const String paywallBackoff = 'paywallBackoff';
}

@ -64,15 +64,6 @@ class ClassController extends BaseController {
(error, stackTrace) =>
ClassCodeUtil.messageSnack(context, ErrorCopy(context, error).body),
);
} else {
try {
//question for gabby: why do we need this in two places?
if (!_pangeaController.subscriptionController.isSubscribed) {
await _pangeaController.subscriptionController.showPaywall(context);
}
} catch (err) {
debugger(when: kDebugMode);
}
}
}

@ -23,11 +23,16 @@ import 'package:http/http.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:url_launcher/url_launcher_string.dart';
enum CanSendStatus {
subscribed,
dimissedPaywall,
showPaywall,
}
class SubscriptionController extends BaseController {
late PangeaController _pangeaController;
SubscriptionInfo? subscription;
//convert this logic to use completer
bool initialized = false;
final StreamController subscriptionStream = StreamController.broadcast();
@ -40,12 +45,6 @@ class SubscriptionController extends BaseController {
(subscription!.currentSubscriptionId != null ||
subscription!.currentSubscription != null);
bool get currentSubscriptionAvailable =>
isSubscribed && subscription?.currentSubscription != null;
bool get currentSubscriptionIsTrial =>
subscription?.currentSubscription?.isTrial ?? false;
Future<void> initialize() async {
try {
if (_pangeaController.matrixState.client.userID == null) {
@ -60,7 +59,7 @@ class SubscriptionController extends BaseController {
: MobileSubscriptionInfo(pangeaController: _pangeaController);
await subscription!.configure();
if (activatedNewUserTrial) {
if (_activatedNewUserTrial) {
setNewUserTrial();
}
@ -94,7 +93,72 @@ class SubscriptionController extends BaseController {
}
}
bool get activatedNewUserTrial =>
void submitSubscriptionChange(
SubscriptionDetails? selectedSubscription,
BuildContext context, {
bool isPromo = false,
}) async {
if (selectedSubscription != null) {
if (selectedSubscription.isTrial) {
activateNewUserTrial();
return;
}
if (kIsWeb) {
if (selectedSubscription.duration == null) {
ErrorHandler.logError(
m: "Tried to subscribe to web SubscriptionDetails with Null duration",
s: StackTrace.current,
);
return;
}
final String paymentLink = await getPaymentLink(
selectedSubscription.duration!,
isPromo: isPromo,
);
_pangeaController.pStoreService.save(
PLocalKey.beganWebPayment,
true,
);
setState();
launchUrlString(
paymentLink,
webOnlyWindowName: "_self",
);
return;
}
if (selectedSubscription.package == null) {
ErrorHandler.logError(
m: "Tried to subscribe to SubscriptionDetails with Null revenuecat Package",
s: StackTrace.current,
);
return;
}
try {
GoogleAnalytics.beginPurchaseSubscription(
selectedSubscription,
context,
);
await Purchases.purchasePackage(selectedSubscription.package!);
GoogleAnalytics.updateUserSubscriptionStatus(true);
} catch (err) {
final errCode = PurchasesErrorHelper.getErrorCode(
err as PlatformException,
);
if (errCode == PurchasesErrorCode.purchaseCancelledError) {
debugPrint("User cancelled purchase");
return;
}
ErrorHandler.logError(
m: "Failed to purchase revenuecat package for user ${_pangeaController.matrixState.client.userID} with error code $errCode",
s: StackTrace.current,
);
return;
}
}
}
bool get _activatedNewUserTrial =>
_pangeaController.userController.inTrialWindow &&
(_pangeaController.pStoreService.read(PLocalKey.activatedTrialKey) ??
false);
@ -123,14 +187,56 @@ class SubscriptionController extends BaseController {
return;
}
await subscription!.setCustomerInfo();
setState();
}
Future<void> showPaywall(
BuildContext context, [
bool forceShow = false,
]) async {
CanSendStatus get canSendStatus => isSubscribed
? CanSendStatus.subscribed
: _shouldShowPaywall
? CanSendStatus.showPaywall
: CanSendStatus.dimissedPaywall;
DateTime? get _lastDismissedPaywall {
final lastDismissed = _pangeaController.pStoreService.read(
PLocalKey.dismissedPaywall,
);
if (lastDismissed == null) return null;
return DateTime.tryParse(lastDismissed);
}
int? get _paywallBackoff {
final backoff = _pangeaController.pStoreService.read(
PLocalKey.paywallBackoff,
);
if (backoff == null) return null;
return backoff;
}
bool get _shouldShowPaywall {
return initialized &&
!isSubscribed &&
(_lastDismissedPaywall == null ||
DateTime.now().difference(_lastDismissedPaywall!).inHours >
(24 * (_paywallBackoff ?? 1)));
}
void dismissPaywall() {
_pangeaController.pStoreService.save(
PLocalKey.dismissedPaywall,
DateTime.now().toString(),
);
if (_paywallBackoff == null) {
_pangeaController.pStoreService.save(PLocalKey.paywallBackoff, 1);
} else {
_pangeaController.pStoreService.save(
PLocalKey.paywallBackoff,
_paywallBackoff! + 1,
);
}
}
Future<void> showPaywall(BuildContext context) async {
try {
if (!initialized) {
await initialize();
@ -138,14 +244,16 @@ class SubscriptionController extends BaseController {
if (subscription?.availableSubscriptions.isEmpty ?? true) {
return;
}
if (!forceShow && isSubscribed) return;
showModalBottomSheet(
if (isSubscribed) return;
await showModalBottomSheet(
isScrollControlled: true,
useRootNavigator: !PlatformInfos.isMobile,
clipBehavior: Clip.hardEdge,
context: context,
constraints: BoxConstraints(
maxHeight: PlatformInfos.isMobile ? 600 : 450,
maxHeight: PlatformInfos.isMobile
? MediaQuery.of(context).size.height - 50
: 600,
),
builder: (_) {
return SubscriptionPaywall(
@ -153,11 +261,28 @@ class SubscriptionController extends BaseController {
);
},
);
dismissPaywall();
} catch (e, s) {
ErrorHandler.logError(e: e, s: s);
}
}
Future<String> getPaymentLink(String 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",
);
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;
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(
@ -183,82 +308,6 @@ class SubscriptionController extends BaseController {
return subscribed;
}
Future<String> getPaymentLink(String 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",
);
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;
if (email != null) {
paymentLink += "?prefilled_email=${Uri.encodeComponent(email)}";
}
return paymentLink;
}
void submitSubscriptionChange(
SubscriptionDetails? selectedSubscription,
BuildContext context, {
bool isPromo = false,
}) async {
if (selectedSubscription != null) {
if (kIsWeb) {
if (selectedSubscription.duration == null) {
ErrorHandler.logError(
m: "Tried to subscribe to web SubscriptionDetails with Null duration",
s: StackTrace.current,
);
return;
}
final String paymentLink = await getPaymentLink(
selectedSubscription.duration!,
isPromo: isPromo,
);
_pangeaController.pStoreService.save(
PLocalKey.beganWebPayment,
true,
);
setState();
launchUrlString(
paymentLink,
webOnlyWindowName: "_self",
);
return;
}
if (selectedSubscription.package == null) {
ErrorHandler.logError(
m: "Tried to subscribe to SubscriptionDetails with Null revenuecat Package",
s: StackTrace.current,
);
return;
}
try {
GoogleAnalytics.beginPurchaseSubscription(
selectedSubscription,
context,
);
await Purchases.purchasePackage(selectedSubscription.package!);
GoogleAnalytics.updateUserSubscriptionStatus(true);
} catch (err) {
final errCode = PurchasesErrorHelper.getErrorCode(
err as PlatformException,
);
if (errCode == PurchasesErrorCode.purchaseCancelledError) {
debugPrint("User cancelled purchase");
return;
}
ErrorHandler.logError(
m: "Failed to purchase revenuecat package for user ${_pangeaController.matrixState.client.userID} with error code $errCode",
s: StackTrace.current,
);
return;
}
}
}
Future<void> redeemPromoCode(BuildContext context) async {
final List<String>? promoCode = await showTextInputDialog(
useRootNavigator: false,

@ -1,13 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart';
import 'package:fluffychat/pangea/widgets/subscription/subscription_buttons.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class ChangeSubscription extends StatelessWidget {
final SubscriptionManagementController controller;
@ -20,26 +16,7 @@ class ChangeSubscription extends StatelessWidget {
@override
Widget build(BuildContext context) {
void submitChange({bool isPromo = false}) {
try {
pangeaController.subscriptionController.submitSubscriptionChange(
controller.selectedSubscription,
context,
isPromo: isPromo,
);
} catch (err) {
showOkAlertDialog(
context: context,
title: L10n.of(context)!.oopsSomethingWentWrong,
message: L10n.of(context)!.errorPleaseRefresh,
okLabel: L10n.of(context)!.close,
);
}
}
return pangeaController.subscriptionController.subscription != null &&
pangeaController.subscriptionController.subscription!
.availableSubscriptions.isNotEmpty
return controller.subscriptionsAvailable
? Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
@ -59,44 +36,25 @@ class ChangeSubscription extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
OutlinedButton(
onPressed: () => submitChange(),
child: Text(L10n.of(context)!.pay),
),
const SizedBox(height: 10),
if (kIsWeb)
OutlinedButton(
onPressed: () => submitChange(isPromo: true),
child: Text(L10n.of(context)!.redeemPromoCode),
onPressed: () => controller.submitChange(),
child: Text(
controller.selectedSubscription!.isTrial
? L10n.of(context)!.activateTrial
: L10n.of(context)!.pay,
),
),
const SizedBox(height: 20),
],
),
),
// if (controller.selectedSubscription != null && Platform.isIOS)
// TextButton(
// onPressed: () {
// try {
// pangeaController.subscriptionController
// .redeemPromoCode(context);
// } catch (err) {
// showOkAlertDialog(
// context: context,
// title: L10n.of(context)!.oopsSomethingWentWrong,
// message: L10n.of(context)!.errorPleaseRefresh,
// okLabel: L10n.of(context)!.close,
// );
// }
// },
// child: Text(L10n.of(context)!.redeemPromoCode),
// )
],
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context)!.oopsSomethingWentWrong),
Text(L10n.of(context)!.errorPleaseRefresh),
],
: const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
);
}

@ -1,14 +1,15 @@
import 'dart:async';
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/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription_view.dart';
import 'package:fluffychat/pangea/utils/subscription_app_id.dart';
import 'package:fluffychat/pangea/widgets/subscription/subscription_snackbar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -21,27 +22,30 @@ class SubscriptionManagement extends StatefulWidget {
}
class SubscriptionManagementController extends State<SubscriptionManagement> {
final PangeaController pangeaController = MatrixState.pangeaController;
final SubscriptionController subscriptionController =
MatrixState.pangeaController.subscriptionController;
SubscriptionDetails? selectedSubscription;
late StreamSubscription _settingsSubscription;
StreamSubscription? _subscriptionStatusStream;
@override
void initState() {
_settingsSubscription =
pangeaController.subscriptionController.stateStream.listen((event) {
if (!subscriptionController.initialized) {
subscriptionController.initialize().then((_) => setState(() {}));
}
_settingsSubscription = subscriptionController.stateStream.listen((event) {
debugPrint("stateStream event in subscription settings");
setState(() {});
});
_subscriptionStatusStream ??= pangeaController
.subscriptionController.subscriptionStream.stream
.listen((_) {
_subscriptionStatusStream ??=
subscriptionController.subscriptionStream.stream.listen((_) {
showSubscribedSnackbar(context);
context.go('/rooms');
});
pangeaController.subscriptionController.updateCustomerInfo();
subscriptionController.updateCustomerInfo();
super.initState();
}
@ -52,44 +56,76 @@ class SubscriptionManagementController extends State<SubscriptionManagement> {
_subscriptionStatusStream?.cancel();
}
bool get subscriptionsAvailable =>
subscriptionController.subscription?.availableSubscriptions.isNotEmpty ??
false;
bool get currentSubscriptionAvailable =>
pangeaController.subscriptionController.currentSubscriptionAvailable;
subscriptionController.isSubscribed &&
subscriptionController.subscription?.currentSubscription != null;
String? get purchasePlatformDisplayName => pangeaController
.subscriptionController.subscription?.purchasePlatformDisplayName;
String? get purchasePlatformDisplayName =>
subscriptionController.subscription?.purchasePlatformDisplayName;
bool get currentSubscriptionIsPromotional =>
pangeaController.subscriptionController.subscription
?.currentSubscriptionIsPromotional ??
subscriptionController.subscription?.currentSubscriptionIsPromotional ??
false;
bool get isNewUserTrial =>
pangeaController.subscriptionController.subscription?.isNewUserTrial ??
false;
subscriptionController.subscription?.isNewUserTrial ?? false;
String get currentSubscriptionTitle =>
subscriptionController.subscription?.currentSubscription
?.displayName(context) ??
"";
String get currentSubscriptionPrice =>
subscriptionController.subscription?.currentSubscription
?.displayPrice(context) ??
"";
bool get showManagementOptions {
if (!currentSubscriptionAvailable) {
return false;
}
if (pangeaController.subscriptionController.subscription!.purchasedOnWeb) {
if (subscriptionController.subscription!.purchasedOnWeb) {
return true;
}
return pangeaController.subscriptionController.subscription!
.currentPlatformMatchesPurchasePlatform;
return subscriptionController
.subscription!.currentPlatformMatchesPurchasePlatform;
}
void submitChange({bool isPromo = false}) {
try {
subscriptionController.submitSubscriptionChange(
selectedSubscription,
context,
isPromo: isPromo,
);
setState(() {});
} catch (err) {
showOkAlertDialog(
context: context,
title: L10n.of(context)!.oopsSomethingWentWrong,
message: L10n.of(context)!.errorPleaseRefresh,
okLabel: L10n.of(context)!.close,
);
}
}
Future<void> launchMangementUrl(ManagementOption option) async {
String managementUrl = Environment.stripeManagementUrl;
final String? email = await pangeaController.userController.userEmail;
final String? email =
await MatrixState.pangeaController.userController.userEmail;
if (email != null) {
managementUrl += "?prefilled_email=${Uri.encodeComponent(email)}";
}
final String? purchaseAppId = pangeaController
.subscriptionController.subscription?.currentSubscription?.appId!;
final String? purchaseAppId =
subscriptionController.subscription?.currentSubscription?.appId!;
if (purchaseAppId == null) return;
final SubscriptionAppIds? appIds =
pangeaController.subscriptionController.subscription!.appIds;
subscriptionController.subscription!.appIds;
if (purchaseAppId == appIds?.stripeId) {
launchUrlString(managementUrl);

@ -1,33 +1,18 @@
// Flutter imports:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:intl/intl.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/pages/settings_subscription/change_subscription.dart';
import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:intl/intl.dart';
class SettingsSubscriptionView extends StatelessWidget {
final SubscriptionManagementController controller;
final PangeaController pangeaController = MatrixState.pangeaController;
SettingsSubscriptionView(this.controller, {super.key});
const SettingsSubscriptionView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final String currentSubscriptionTitle = pangeaController
.subscriptionController.subscription?.currentSubscription
?.displayName(context) ??
"";
final String currentSubscriptionPrice = pangeaController
.subscriptionController.subscription?.currentSubscription
?.displayPrice(context) ??
"";
return Scaffold(
appBar: AppBar(
centerTitle: true,
@ -38,17 +23,15 @@ class SettingsSubscriptionView extends StatelessWidget {
body: ListTileTheme(
iconColor: Theme.of(context).textTheme.bodyLarge!.color,
child: MaxWidthBody(
child: !(pangeaController.subscriptionController.isSubscribed)
child: !(controller.subscriptionController.isSubscribed)
? ChangeSubscription(controller: controller)
: Column(
children: [
if (pangeaController.subscriptionController.subscription!
.currentSubscription !=
null)
if (controller.currentSubscriptionAvailable)
ListTile(
title: Text(L10n.of(context)!.currentSubscription),
subtitle: Text(currentSubscriptionTitle),
trailing: Text(currentSubscriptionPrice),
subtitle: Text(controller.currentSubscriptionTitle),
trailing: Text(controller.currentSubscriptionPrice),
),
Column(
children: [
@ -84,8 +67,6 @@ class SettingsSubscriptionView extends StatelessWidget {
if (!(controller.showManagementOptions))
ManagementNotAvailableWarning(
controller: controller,
subscriptionController:
pangeaController.subscriptionController,
),
],
),
@ -97,11 +78,9 @@ class SettingsSubscriptionView extends StatelessWidget {
class ManagementNotAvailableWarning extends StatelessWidget {
final SubscriptionManagementController controller;
final SubscriptionController subscriptionController;
const ManagementNotAvailableWarning({
required this.controller,
required this.subscriptionController,
super.key,
});
@ -112,7 +91,7 @@ class ManagementNotAvailableWarning extends StatelessWidget {
if (controller.isNewUserTrial) {
return L10n.of(context)!.trialExpiration(
formatter.format(
subscriptionController.subscription!.expirationDate!,
controller.subscriptionController.subscription!.expirationDate!,
),
);
}
@ -125,13 +104,14 @@ class ManagementNotAvailableWarning extends StatelessWidget {
return warningText;
}
if (controller.currentSubscriptionIsPromotional) {
if (subscriptionController.subscription?.isLifetimeSubscription ??
if (controller
.subscriptionController.subscription?.isLifetimeSubscription ??
false) {
return L10n.of(context)!.promotionalSubscriptionDesc;
}
return L10n.of(context)!.promoSubscriptionExpirationDesc(
formatter.format(
subscriptionController.subscription!.expirationDate!,
controller.subscriptionController.subscription!.expirationDate!,
),
);
}

@ -1,5 +1,8 @@
import 'dart:developer';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/models/igc_text_data_model.dart';
import 'package:fluffychat/pangea/widgets/igc/paywall_card.dart';
import 'package:fluffychat/pangea/widgets/igc/span_card.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -40,6 +43,19 @@ class PangeaTextController extends TextEditingController {
debugger(when: kDebugMode);
return;
}
final CanSendStatus canSendStatus =
choreographer.pangeaController.subscriptionController.canSendStatus;
if (canSendStatus == CanSendStatus.showPaywall &&
!choreographer.isFetching &&
text.isNotEmpty) {
OverlayUtil.showPositionedCard(
context: context,
cardToShow: const PaywallCard(),
cardSize: const Size(325, 375),
transformTargetId: choreographer.inputTransformTargetKey,
);
}
if (choreographer.igc.igcTextData == null) return;
// debugPrint(
@ -113,7 +129,20 @@ class PangeaTextController extends TextEditingController {
// debugPrint("composing after ${value.composing.textAfter(value.text)}");
// }
if (choreographer.igc.igcTextData == null || text.isEmpty) {
final CanSendStatus canSendStatus =
choreographer.pangeaController.subscriptionController.canSendStatus;
if (canSendStatus == CanSendStatus.showPaywall &&
!choreographer.isFetching &&
text.isNotEmpty) {
return TextSpan(
text: text,
style: style?.merge(
IGCTextData.underlineStyle(
const Color.fromARGB(187, 132, 96, 224),
),
),
);
} else if (choreographer.igc.igcTextData == null || text.isEmpty) {
return TextSpan(text: text, style: style);
} else {
final parts = text.split(choreographer.igc.igcTextData!.originalInput);

@ -0,0 +1,123 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart';
import 'package:fluffychat/pangea/widgets/igc/card_header.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:shimmer/shimmer.dart';
class PaywallCard extends StatelessWidget {
const PaywallCard({
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CardHeader(
text: L10n.of(context)!.subscriptionPopupTitle,
botExpression: BotExpression.addled,
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const OptionsShimmer(),
const SizedBox(height: 15.0),
Text(
L10n.of(context)!.subscriptionPopupDesc,
style: BotStyle.text(context),
textAlign: TextAlign.center,
),
const SizedBox(height: 15.0),
SizedBox(
width: double.infinity,
child: TextButton(
onPressed: () {
MatrixState.pangeaController.subscriptionController
.showPaywall(context);
MatrixState.pAnyState.closeOverlay();
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
(AppConfig.primaryColor).withOpacity(0.1),
),
),
child: Text(L10n.of(context)!.seeOptions),
),
),
const SizedBox(height: 5.0),
SizedBox(
width: double.infinity,
child: TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
AppConfig.primaryColor.withOpacity(0.1),
),
),
onPressed: () {
MatrixState.pangeaController.subscriptionController
.dismissPaywall();
MatrixState.pAnyState.closeOverlay();
},
child: Center(
child: Text(L10n.of(context)!.continuedWithoutSubscription),
),
),
),
],
),
),
],
);
}
}
class OptionsShimmer extends StatelessWidget {
const OptionsShimmer({super.key});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Theme.of(context).colorScheme.primary.withOpacity(0.5),
highlightColor: Theme.of(context).colorScheme.primary.withOpacity(0.1),
direction: ShimmerDirection.ltr,
child: Wrap(
alignment: WrapAlignment.center,
children: List.generate(
3,
(_) => Container(
margin: const EdgeInsets.all(2),
padding: EdgeInsets.zero,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 7),
),
backgroundColor: MaterialStateProperty.all<Color>(
Theme.of(context).colorScheme.primary.withOpacity(0.1),
),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
onPressed: () {},
child: Text(
"",
style: BotStyle.text(context),
),
),
),
),
),
);
}
}

@ -1,11 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class SubscriptionButtons extends StatelessWidget {
final SubscriptionManagementController controller;
@ -17,43 +15,40 @@ class SubscriptionButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bool inTrialWindow = pangeaController.userController.inTrialWindow;
return ListView.builder(
shrinkWrap: true,
itemCount: pangeaController
itemCount: controller
.subscriptionController.subscription!.availableSubscriptions.length,
itemBuilder: (BuildContext context, int i) => Column(
children: [
ListTile(
title: pangeaController.subscriptionController.subscription!
.availableSubscriptions[i].isTrial
? Text(L10n.of(context)!.oneWeekTrial)
: Text(
pangeaController.subscriptionController.subscription!
.availableSubscriptions[i]
.displayName(context),
),
subtitle: Text(
pangeaController.subscriptionController.subscription!
.availableSubscriptions[i]
.displayPrice(context),
itemBuilder: (BuildContext context, int i) {
final SubscriptionDetails subscription = pangeaController
.subscriptionController.subscription!.availableSubscriptions[i];
return Column(
children: [
ListTile(
title: subscription.isTrial
? Text(L10n.of(context)!.oneWeekTrial)
: Text(
subscription.displayName(context),
),
subtitle: Text(
subscription.isTrial && !inTrialWindow
? L10n.of(context)!.trialPeriodExpired
: subscription.displayPrice(context),
),
trailing: const Icon(Icons.keyboard_arrow_right_outlined),
selected: controller.selectedSubscription == subscription,
selectedTileColor:
Theme.of(context).colorScheme.secondary.withAlpha(16),
enabled: !subscription.isTrial || inTrialWindow,
onTap: () {
controller.selectSubscription(subscription);
},
),
trailing: const Icon(Icons.keyboard_arrow_right_outlined),
selected: controller.selectedSubscription ==
pangeaController.subscriptionController.subscription!
.availableSubscriptions[i],
selectedTileColor:
Theme.of(context).colorScheme.secondary.withAlpha(16),
onTap: () {
final SubscriptionDetails selected = pangeaController
.subscriptionController
.subscription!
.availableSubscriptions[i];
controller.selectSubscription(selected);
},
),
const Divider(height: 1),
],
),
const Divider(height: 1),
],
);
},
);
}
}

@ -18,68 +18,121 @@ class SubscriptionOptions extends StatelessWidget {
return Wrap(
alignment: WrapAlignment.center,
direction: Axis.horizontal,
children: pangeaController
.subscriptionController.subscription!.availableSubscriptions
.map(
(subscription) => SubscriptionCard(
subscription: subscription,
pangeaController: pangeaController,
),
)
.toList(),
spacing: 10,
children: pangeaController.userController.inTrialWindow
? [
SubscriptionCard(
onTap: () => pangeaController.subscriptionController
.submitSubscriptionChange(
SubscriptionDetails(
price: 0,
id: "",
periodType: 'trial',
),
context,
),
title: L10n.of(context)!.freeTrial,
description: L10n.of(context)!.freeTrialDesc,
buttonText: L10n.of(context)!.activateTrial,
),
]
: pangeaController
.subscriptionController.subscription!.availableSubscriptions
.map(
(subscription) => SubscriptionCard(
subscription: subscription,
onTap: () {
pangeaController.subscriptionController
.submitSubscriptionChange(
subscription,
context,
);
},
title: subscription.isTrial
? L10n.of(context)!.oneWeekTrial
: subscription.displayName(context),
enabled: !subscription.isTrial,
description: subscription.isTrial
? L10n.of(context)!.trialPeriodExpired
: null,
),
)
.toList(),
);
}
}
class SubscriptionCard extends StatelessWidget {
final SubscriptionDetails subscription;
final PangeaController pangeaController;
final SubscriptionDetails? subscription;
final void Function()? onTap;
final String? title;
final String? description;
final String? buttonText;
final bool enabled;
const SubscriptionCard({
super.key,
required this.subscription,
required this.pangeaController,
this.subscription,
required this.onTap,
this.title,
this.description,
this.buttonText,
this.enabled = true,
});
@override
Widget build(BuildContext context) {
final ButtonStyle buttonStyle = OutlinedButton.styleFrom(
side: enabled ? null : BorderSide(color: Colors.grey[600]!),
foregroundColor: Colors.white,
backgroundColor: AppConfig.primaryColor,
disabledForegroundColor: const Color.fromARGB(255, 200, 200, 200),
disabledBackgroundColor: Colors.grey[600],
);
return Card(
shape: RoundedRectangleBorder(
side: BorderSide(
color: AppConfig.primaryColorLight.withAlpha(64),
),
borderRadius: const BorderRadius.all(Radius.zero),
color: enabled ? null : const Color.fromARGB(255, 245, 244, 244),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: SizedBox(
height: 250,
width: AppConfig.columnWidth * 0.75,
width: AppConfig.columnWidth * 0.6,
height: 200,
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
subscription.isTrial
? L10n.of(context)!.oneWeekTrial
: subscription.displayName(context),
title ?? subscription?.displayName(context) ?? '',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 24),
style: TextStyle(
fontSize: 24,
color:
enabled ? null : const Color.fromARGB(255, 174, 174, 174),
),
),
Text(
subscription.displayPrice(context),
description ?? subscription?.displayPrice(context) ?? '',
textAlign: TextAlign.center,
style: TextStyle(
color:
enabled ? null : const Color.fromARGB(255, 174, 174, 174),
),
),
OutlinedButton(
onPressed: () {
pangeaController.subscriptionController
.submitSubscriptionChange(
subscription,
context,
);
Navigator.of(context).pop();
},
child: Text(L10n.of(context)!.subscribe),
onPressed: enabled
? () {
if (onTap != null) onTap!();
Navigator.of(context).pop();
}
: null,
style: buttonStyle,
child: Text(
buttonText ?? L10n.of(context)!.subscribe,
),
),
],
),

@ -1,6 +1,5 @@
// Flutter imports:
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/widgets/subscription/subscription_options.dart';
import 'package:flutter/material.dart';
@ -18,89 +17,38 @@ class SubscriptionPaywall extends StatelessWidget {
return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: CloseButton(onPressed: Navigator.of(context).pop),
leading: const CloseButton(),
title: Text(
L10n.of(context)!.getAccess,
style: const TextStyle(
fontSize: 20,
),
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20),
),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: ListView(
children: [
if (pangeaController.matrixState.client.rooms.length > 1) ...[
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (pangeaController.matrixState.client.rooms.length > 1) ...[
Text(
L10n.of(context)!.welcomeBack,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 20),
],
Text(
L10n.of(context)!.welcomeBack,
L10n.of(context)!.subscriptionDesc,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 20),
],
Text(
L10n.of(context)!.subscriptionDesc,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 20),
pangeaController.userController.inTrialWindow
? FreeTrialCard(
pangeaController: pangeaController,
)
: SubscriptionOptions(
pangeaController: pangeaController,
),
],
),
),
);
}
}
class FreeTrialCard extends StatelessWidget {
final PangeaController pangeaController;
const FreeTrialCard({super.key, required this.pangeaController});
@override
Widget build(BuildContext context) {
return Align(
child: Card(
shape: RoundedRectangleBorder(
side: BorderSide(
color: AppConfig.primaryColorLight.withAlpha(64),
),
borderRadius: const BorderRadius.all(Radius.zero),
),
child: SizedBox(
height: 250,
width: AppConfig.columnWidth * 0.75,
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
L10n.of(context)!.freeTrial,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 24),
const SizedBox(height: 40),
Center(
child: SubscriptionOptions(
pangeaController: pangeaController,
),
Text(
L10n.of(context)!.freeTrialDesc,
textAlign: TextAlign.center,
),
OutlinedButton(
onPressed: () {
pangeaController.subscriptionController
.activateNewUserTrial();
Navigator.of(context).pop();
},
child: Text(L10n.of(context)!.activateTrial),
),
],
),
),
],
),
),
),

@ -1964,6 +1964,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shimmer:
dependency: "direct main"
description:
name: shimmer
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter

@ -119,6 +119,7 @@ dependencies:
open_file: ^3.3.2
purchases_flutter: ^5.6.0
sentry_flutter: ^7.4.0
shimmer: ^3.0.0
syncfusion_flutter_datepicker: ^23.2.7
syncfusion_flutter_xlsio: ^23.2.7
# Pangea#

Loading…
Cancel
Save