Merge pull request #68 from pangeachat/trial-ui
updates to subscription paywall flow
This commit is contained in:
commit
7ad50e07e2
17 changed files with 583 additions and 411 deletions
|
|
@ -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,117 +93,17 @@ class SubscriptionController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
bool get activatedNewUserTrial =>
|
||||
_pangeaController.userController.inTrialWindow &&
|
||||
(_pangeaController.pStoreService.read(PLocalKey.activatedTrialKey) ??
|
||||
false);
|
||||
|
||||
void activateNewUserTrial() {
|
||||
_pangeaController.pStoreService.save(PLocalKey.activatedTrialKey, true);
|
||||
setNewUserTrial();
|
||||
}
|
||||
|
||||
void setNewUserTrial() {
|
||||
final String profileCreatedAt =
|
||||
_pangeaController.userController.userModel!.profile!.createdAt;
|
||||
final DateTime creationTimestamp = DateTime.parse(profileCreatedAt);
|
||||
final DateTime expirationDate = creationTimestamp.add(
|
||||
const Duration(days: 7),
|
||||
);
|
||||
subscription?.setTrial(expirationDate);
|
||||
}
|
||||
|
||||
Future<void> updateCustomerInfo() async {
|
||||
if (subscription == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "Null subscription info in subscription settings",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await subscription!.setCustomerInfo();
|
||||
|
||||
setState();
|
||||
}
|
||||
|
||||
Future<void> showPaywall(
|
||||
BuildContext context, [
|
||||
bool forceShow = false,
|
||||
]) async {
|
||||
try {
|
||||
if (!initialized) {
|
||||
await initialize();
|
||||
}
|
||||
if (subscription?.availableSubscriptions.isEmpty ?? true) {
|
||||
return;
|
||||
}
|
||||
if (!forceShow && isSubscribed) return;
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: !PlatformInfos.isMobile,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
context: context,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: PlatformInfos.isMobile ? 600 : 450,
|
||||
),
|
||||
builder: (_) {
|
||||
return SubscriptionPaywall(
|
||||
pangeaController: _pangeaController,
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
}
|
||||
}
|
||||
|
||||
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}",
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 (selectedSubscription.isTrial) {
|
||||
activateNewUserTrial();
|
||||
return;
|
||||
}
|
||||
|
||||
if (kIsWeb) {
|
||||
if (selectedSubscription.duration == null) {
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -259,6 +158,156 @@ class SubscriptionController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
bool get _activatedNewUserTrial =>
|
||||
_pangeaController.userController.inTrialWindow &&
|
||||
(_pangeaController.pStoreService.read(PLocalKey.activatedTrialKey) ??
|
||||
false);
|
||||
|
||||
void activateNewUserTrial() {
|
||||
_pangeaController.pStoreService.save(PLocalKey.activatedTrialKey, true);
|
||||
setNewUserTrial();
|
||||
}
|
||||
|
||||
void setNewUserTrial() {
|
||||
final String profileCreatedAt =
|
||||
_pangeaController.userController.userModel!.profile!.createdAt;
|
||||
final DateTime creationTimestamp = DateTime.parse(profileCreatedAt);
|
||||
final DateTime expirationDate = creationTimestamp.add(
|
||||
const Duration(days: 7),
|
||||
);
|
||||
subscription?.setTrial(expirationDate);
|
||||
}
|
||||
|
||||
Future<void> updateCustomerInfo() async {
|
||||
if (subscription == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "Null subscription info in subscription settings",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await subscription!.setCustomerInfo();
|
||||
setState();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
if (subscription?.availableSubscriptions.isEmpty ?? true) {
|
||||
return;
|
||||
}
|
||||
if (isSubscribed) return;
|
||||
await showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: !PlatformInfos.isMobile,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
context: context,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: PlatformInfos.isMobile
|
||||
? MediaQuery.of(context).size.height - 50
|
||||
: 600,
|
||||
),
|
||||
builder: (_) {
|
||||
return SubscriptionPaywall(
|
||||
pangeaController: _pangeaController,
|
||||
);
|
||||
},
|
||||
);
|
||||
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(
|
||||
"${PApiUrls.subscriptionExpiration}?pangea_user_id=${_pangeaController.matrixState.client.userID}",
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 currentSubscriptionAvailable =>
|
||||
pangeaController.subscriptionController.currentSubscriptionAvailable;
|
||||
bool get subscriptionsAvailable =>
|
||||
subscriptionController.subscription?.availableSubscriptions.isNotEmpty ??
|
||||
false;
|
||||
|
||||
String? get purchasePlatformDisplayName => pangeaController
|
||||
.subscriptionController.subscription?.purchasePlatformDisplayName;
|
||||
bool get currentSubscriptionAvailable =>
|
||||
subscriptionController.isSubscribed &&
|
||||
subscriptionController.subscription?.currentSubscription != null;
|
||||
|
||||
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);
|
||||
|
|
|
|||
123
lib/pangea/widgets/igc/paywall_card.dart
Normal file
123
lib/pangea/widgets/igc/paywall_card.dart
Normal file
|
|
@ -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),
|
||||
const SizedBox(height: 40),
|
||||
Center(
|
||||
child: SubscriptionOptions(
|
||||
pangeaController: pangeaController,
|
||||
),
|
||||
),
|
||||
],
|
||||
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),
|
||||
),
|
||||
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…
Add table
Reference in a new issue