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": {
|
"placeholders": {
|
||||||
"sender": {}
|
"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:async';
|
||||||
import 'dart:developer';
|
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/pages/chat/chat.dart';
|
||||||
import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart';
|
import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart';
|
||||||
import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.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/language_keys.dart';
|
||||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||||
import 'package:fluffychat/pangea/controllers/pangea_controller.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/enum/edit_type.dart';
|
||||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||||
import 'package:fluffychat/pangea/models/class_model.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/models/widget_measurement.dart';
|
||||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||||
import 'package:fluffychat/pangea/utils/error_handler.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 '../../../widgets/matrix.dart';
|
||||||
import '../../enum/use_type.dart';
|
import '../../enum/use_type.dart';
|
||||||
import '../../models/choreo_record.dart';
|
import '../../models/choreo_record.dart';
|
||||||
|
|
@ -51,8 +53,6 @@ class Choreographer {
|
||||||
ChoreoMode choreoMode = ChoreoMode.igc;
|
ChoreoMode choreoMode = ChoreoMode.igc;
|
||||||
final StreamController stateListener = StreamController();
|
final StreamController stateListener = StreamController();
|
||||||
|
|
||||||
bool toldToPay = false;
|
|
||||||
|
|
||||||
Choreographer(this.pangeaController, this.chatController) {
|
Choreographer(this.pangeaController, this.chatController) {
|
||||||
_initialize();
|
_initialize();
|
||||||
}
|
}
|
||||||
|
|
@ -71,9 +71,14 @@ class Choreographer {
|
||||||
void send(BuildContext context) {
|
void send(BuildContext context) {
|
||||||
if (isFetching) return;
|
if (isFetching) return;
|
||||||
|
|
||||||
if (!pangeaController.subscriptionController.isSubscribed && !toldToPay) {
|
if (pangeaController.subscriptionController.canSendStatus ==
|
||||||
toldToPay = true;
|
CanSendStatus.showPaywall) {
|
||||||
pangeaController.subscriptionController.showPaywall(context);
|
OverlayUtil.showPositionedCard(
|
||||||
|
context: context,
|
||||||
|
cardToShow: const PaywallCard(),
|
||||||
|
cardSize: const Size(325, 375),
|
||||||
|
transformTargetId: inputTransformTargetKey,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,22 +204,20 @@ class Choreographer {
|
||||||
Future<void> getLanguageHelp([bool tokensOnly = false]) async {
|
Future<void> getLanguageHelp([bool tokensOnly = false]) async {
|
||||||
try {
|
try {
|
||||||
if (errorService.isError) return;
|
if (errorService.isError) return;
|
||||||
if (!pangeaController.subscriptionController.isSubscribed &&
|
final CanSendStatus canSendStatus =
|
||||||
pangeaController.subscriptionController.initialized) {
|
pangeaController.subscriptionController.canSendStatus;
|
||||||
debugPrint('setting not subscribed error');
|
|
||||||
errorService.setErrorAndLock(
|
if (canSendStatus != CanSendStatus.subscribed) {
|
||||||
ChoreoError(
|
|
||||||
type: ChoreoErrorType.unsubscribed,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
startLoading();
|
startLoading();
|
||||||
if (choreoMode == ChoreoMode.it &&
|
if (choreoMode == ChoreoMode.it &&
|
||||||
itController.isTranslationDone &&
|
itController.isTranslationDone &&
|
||||||
!tokensOnly) {
|
!tokensOnly) {
|
||||||
debugger(when: kDebugMode);
|
debugger(when: kDebugMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
await (choreoMode == ChoreoMode.it && !itController.isTranslationDone
|
await (choreoMode == ChoreoMode.it && !itController.isTranslationDone
|
||||||
? itController.getTranslationData(_useCustomInput)
|
? itController.getTranslationData(_useCustomInput)
|
||||||
: igc.getIGCTextData(tokensOnly: tokensOnly));
|
: igc.getIGCTextData(tokensOnly: tokensOnly));
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
|
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
|
||||||
import '../../utils/error_handler.dart';
|
import '../../utils/error_handler.dart';
|
||||||
|
|
||||||
enum ChoreoErrorType {
|
enum ChoreoErrorType {
|
||||||
unknown,
|
unknown,
|
||||||
classDisabled,
|
classDisabled,
|
||||||
userDisabled,
|
userDisabled,
|
||||||
unsubscribed,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChoreoError {
|
class ChoreoError {
|
||||||
|
|
@ -22,8 +21,6 @@ class ChoreoError {
|
||||||
return "Class Disabled";
|
return "Class Disabled";
|
||||||
case ChoreoErrorType.userDisabled:
|
case ChoreoErrorType.userDisabled:
|
||||||
return "User Disabled";
|
return "User Disabled";
|
||||||
case ChoreoErrorType.unsubscribed:
|
|
||||||
return "Unsubscribed";
|
|
||||||
default:
|
default:
|
||||||
return ErrorCopy(context, raw).title;
|
return ErrorCopy(context, raw).title;
|
||||||
}
|
}
|
||||||
|
|
@ -35,8 +32,6 @@ class ChoreoError {
|
||||||
return "Class Disabled";
|
return "Class Disabled";
|
||||||
case ChoreoErrorType.userDisabled:
|
case ChoreoErrorType.userDisabled:
|
||||||
return "User Disabled";
|
return "User Disabled";
|
||||||
case ChoreoErrorType.unsubscribed:
|
|
||||||
return "Unsubscribed";
|
|
||||||
default:
|
default:
|
||||||
return ErrorCopy(context, raw).body;
|
return ErrorCopy(context, raw).body;
|
||||||
}
|
}
|
||||||
|
|
@ -48,8 +43,6 @@ class ChoreoError {
|
||||||
return Icons.history_edu_outlined;
|
return Icons.history_edu_outlined;
|
||||||
case ChoreoErrorType.userDisabled:
|
case ChoreoErrorType.userDisabled:
|
||||||
return Icons.history_edu_outlined;
|
return Icons.history_edu_outlined;
|
||||||
case ChoreoErrorType.unsubscribed:
|
|
||||||
return Icons.lock_outline;
|
|
||||||
default:
|
default:
|
||||||
return Icons.error_outline;
|
return Icons.error_outline;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,6 @@ class ChoreographerHasErrorButton extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (error.type == ChoreoErrorType.unsubscribed) {
|
|
||||||
pangeaController.subscriptionController.showPaywall(context);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mini: true,
|
mini: true,
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,6 @@ class PLocalKey {
|
||||||
|
|
||||||
// making this a random string so that it's harder to guess
|
// making this a random string so that it's harder to guess
|
||||||
static const String activatedTrialKey = '7C4EuKIsph';
|
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) =>
|
(error, stackTrace) =>
|
||||||
ClassCodeUtil.messageSnack(context, ErrorCopy(context, error).body),
|
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:purchases_flutter/purchases_flutter.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
enum CanSendStatus {
|
||||||
|
subscribed,
|
||||||
|
dimissedPaywall,
|
||||||
|
showPaywall,
|
||||||
|
}
|
||||||
|
|
||||||
class SubscriptionController extends BaseController {
|
class SubscriptionController extends BaseController {
|
||||||
late PangeaController _pangeaController;
|
late PangeaController _pangeaController;
|
||||||
SubscriptionInfo? subscription;
|
SubscriptionInfo? subscription;
|
||||||
|
|
||||||
//convert this logic to use completer
|
|
||||||
bool initialized = false;
|
bool initialized = false;
|
||||||
final StreamController subscriptionStream = StreamController.broadcast();
|
final StreamController subscriptionStream = StreamController.broadcast();
|
||||||
|
|
||||||
|
|
@ -40,12 +45,6 @@ class SubscriptionController extends BaseController {
|
||||||
(subscription!.currentSubscriptionId != null ||
|
(subscription!.currentSubscriptionId != null ||
|
||||||
subscription!.currentSubscription != null);
|
subscription!.currentSubscription != null);
|
||||||
|
|
||||||
bool get currentSubscriptionAvailable =>
|
|
||||||
isSubscribed && subscription?.currentSubscription != null;
|
|
||||||
|
|
||||||
bool get currentSubscriptionIsTrial =>
|
|
||||||
subscription?.currentSubscription?.isTrial ?? false;
|
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
try {
|
try {
|
||||||
if (_pangeaController.matrixState.client.userID == null) {
|
if (_pangeaController.matrixState.client.userID == null) {
|
||||||
|
|
@ -60,7 +59,7 @@ class SubscriptionController extends BaseController {
|
||||||
: MobileSubscriptionInfo(pangeaController: _pangeaController);
|
: MobileSubscriptionInfo(pangeaController: _pangeaController);
|
||||||
|
|
||||||
await subscription!.configure();
|
await subscription!.configure();
|
||||||
if (activatedNewUserTrial) {
|
if (_activatedNewUserTrial) {
|
||||||
setNewUserTrial();
|
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(
|
void submitSubscriptionChange(
|
||||||
SubscriptionDetails? selectedSubscription,
|
SubscriptionDetails? selectedSubscription,
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
bool isPromo = false,
|
bool isPromo = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (selectedSubscription != null) {
|
if (selectedSubscription != null) {
|
||||||
|
if (selectedSubscription.isTrial) {
|
||||||
|
activateNewUserTrial();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
if (selectedSubscription.duration == null) {
|
if (selectedSubscription.duration == null) {
|
||||||
ErrorHandler.logError(
|
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 {
|
Future<void> redeemPromoCode(BuildContext context) async {
|
||||||
final List<String>? promoCode = await showTextInputDialog(
|
final List<String>? promoCode = await showTextInputDialog(
|
||||||
useRootNavigator: false,
|
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/controllers/pangea_controller.dart';
|
||||||
import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart';
|
import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart';
|
||||||
import 'package:fluffychat/pangea/widgets/subscription/subscription_buttons.dart';
|
import 'package:fluffychat/pangea/widgets/subscription/subscription_buttons.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
||||||
class ChangeSubscription extends StatelessWidget {
|
class ChangeSubscription extends StatelessWidget {
|
||||||
final SubscriptionManagementController controller;
|
final SubscriptionManagementController controller;
|
||||||
|
|
@ -20,26 +16,7 @@ class ChangeSubscription extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
void submitChange({bool isPromo = false}) {
|
return controller.subscriptionsAvailable
|
||||||
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
|
|
||||||
? Column(
|
? Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
|
@ -59,44 +36,25 @@ class ChangeSubscription extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () => submitChange(),
|
onPressed: () => controller.submitChange(),
|
||||||
child: Text(L10n.of(context)!.pay),
|
child: Text(
|
||||||
),
|
controller.selectedSubscription!.isTrial
|
||||||
const SizedBox(height: 10),
|
? L10n.of(context)!.activateTrial
|
||||||
if (kIsWeb)
|
: L10n.of(context)!.pay,
|
||||||
OutlinedButton(
|
|
||||||
onPressed: () => submitChange(isPromo: true),
|
|
||||||
child: Text(L10n.of(context)!.redeemPromoCode),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
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(
|
: const Center(
|
||||||
child: Column(
|
child: Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: EdgeInsets.all(16.0),
|
||||||
children: [
|
child: CircularProgressIndicator.adaptive(
|
||||||
Text(L10n.of(context)!.oopsSomethingWentWrong),
|
strokeWidth: 2,
|
||||||
Text(L10n.of(context)!.errorPleaseRefresh),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
import 'package:fluffychat/pangea/config/environment.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/controllers/subscription_controller.dart';
|
||||||
import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription_view.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/utils/subscription_app_id.dart';
|
||||||
import 'package:fluffychat/pangea/widgets/subscription/subscription_snackbar.dart';
|
import 'package:fluffychat/pangea/widgets/subscription/subscription_snackbar.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
|
@ -21,27 +22,30 @@ class SubscriptionManagement extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubscriptionManagementController extends State<SubscriptionManagement> {
|
class SubscriptionManagementController extends State<SubscriptionManagement> {
|
||||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
final SubscriptionController subscriptionController =
|
||||||
|
MatrixState.pangeaController.subscriptionController;
|
||||||
SubscriptionDetails? selectedSubscription;
|
SubscriptionDetails? selectedSubscription;
|
||||||
late StreamSubscription _settingsSubscription;
|
late StreamSubscription _settingsSubscription;
|
||||||
StreamSubscription? _subscriptionStatusStream;
|
StreamSubscription? _subscriptionStatusStream;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_settingsSubscription =
|
if (!subscriptionController.initialized) {
|
||||||
pangeaController.subscriptionController.stateStream.listen((event) {
|
subscriptionController.initialize().then((_) => setState(() {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
_settingsSubscription = subscriptionController.stateStream.listen((event) {
|
||||||
debugPrint("stateStream event in subscription settings");
|
debugPrint("stateStream event in subscription settings");
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
|
|
||||||
_subscriptionStatusStream ??= pangeaController
|
_subscriptionStatusStream ??=
|
||||||
.subscriptionController.subscriptionStream.stream
|
subscriptionController.subscriptionStream.stream.listen((_) {
|
||||||
.listen((_) {
|
|
||||||
showSubscribedSnackbar(context);
|
showSubscribedSnackbar(context);
|
||||||
context.go('/rooms');
|
context.go('/rooms');
|
||||||
});
|
});
|
||||||
|
|
||||||
pangeaController.subscriptionController.updateCustomerInfo();
|
subscriptionController.updateCustomerInfo();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,44 +56,76 @@ class SubscriptionManagementController extends State<SubscriptionManagement> {
|
||||||
_subscriptionStatusStream?.cancel();
|
_subscriptionStatusStream?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get currentSubscriptionAvailable =>
|
bool get subscriptionsAvailable =>
|
||||||
pangeaController.subscriptionController.currentSubscriptionAvailable;
|
subscriptionController.subscription?.availableSubscriptions.isNotEmpty ??
|
||||||
|
false;
|
||||||
|
|
||||||
String? get purchasePlatformDisplayName => pangeaController
|
bool get currentSubscriptionAvailable =>
|
||||||
.subscriptionController.subscription?.purchasePlatformDisplayName;
|
subscriptionController.isSubscribed &&
|
||||||
|
subscriptionController.subscription?.currentSubscription != null;
|
||||||
|
|
||||||
|
String? get purchasePlatformDisplayName =>
|
||||||
|
subscriptionController.subscription?.purchasePlatformDisplayName;
|
||||||
|
|
||||||
bool get currentSubscriptionIsPromotional =>
|
bool get currentSubscriptionIsPromotional =>
|
||||||
pangeaController.subscriptionController.subscription
|
subscriptionController.subscription?.currentSubscriptionIsPromotional ??
|
||||||
?.currentSubscriptionIsPromotional ??
|
|
||||||
false;
|
false;
|
||||||
|
|
||||||
bool get isNewUserTrial =>
|
bool get isNewUserTrial =>
|
||||||
pangeaController.subscriptionController.subscription?.isNewUserTrial ??
|
subscriptionController.subscription?.isNewUserTrial ?? false;
|
||||||
false;
|
|
||||||
|
String get currentSubscriptionTitle =>
|
||||||
|
subscriptionController.subscription?.currentSubscription
|
||||||
|
?.displayName(context) ??
|
||||||
|
"";
|
||||||
|
|
||||||
|
String get currentSubscriptionPrice =>
|
||||||
|
subscriptionController.subscription?.currentSubscription
|
||||||
|
?.displayPrice(context) ??
|
||||||
|
"";
|
||||||
|
|
||||||
bool get showManagementOptions {
|
bool get showManagementOptions {
|
||||||
if (!currentSubscriptionAvailable) {
|
if (!currentSubscriptionAvailable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (pangeaController.subscriptionController.subscription!.purchasedOnWeb) {
|
if (subscriptionController.subscription!.purchasedOnWeb) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return pangeaController.subscriptionController.subscription!
|
return subscriptionController
|
||||||
.currentPlatformMatchesPurchasePlatform;
|
.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 {
|
Future<void> launchMangementUrl(ManagementOption option) async {
|
||||||
String managementUrl = Environment.stripeManagementUrl;
|
String managementUrl = Environment.stripeManagementUrl;
|
||||||
final String? email = await pangeaController.userController.userEmail;
|
final String? email =
|
||||||
|
await MatrixState.pangeaController.userController.userEmail;
|
||||||
if (email != null) {
|
if (email != null) {
|
||||||
managementUrl += "?prefilled_email=${Uri.encodeComponent(email)}";
|
managementUrl += "?prefilled_email=${Uri.encodeComponent(email)}";
|
||||||
}
|
}
|
||||||
final String? purchaseAppId = pangeaController
|
final String? purchaseAppId =
|
||||||
.subscriptionController.subscription?.currentSubscription?.appId!;
|
subscriptionController.subscription?.currentSubscription?.appId!;
|
||||||
if (purchaseAppId == null) return;
|
if (purchaseAppId == null) return;
|
||||||
|
|
||||||
final SubscriptionAppIds? appIds =
|
final SubscriptionAppIds? appIds =
|
||||||
pangeaController.subscriptionController.subscription!.appIds;
|
subscriptionController.subscription!.appIds;
|
||||||
|
|
||||||
if (purchaseAppId == appIds?.stripeId) {
|
if (purchaseAppId == appIds?.stripeId) {
|
||||||
launchUrlString(managementUrl);
|
launchUrlString(managementUrl);
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,18 @@
|
||||||
// Flutter imports:
|
// 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/change_subscription.dart';
|
||||||
import 'package:fluffychat/pangea/pages/settings_subscription/settings_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/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 {
|
class SettingsSubscriptionView extends StatelessWidget {
|
||||||
final SubscriptionManagementController controller;
|
final SubscriptionManagementController controller;
|
||||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
const SettingsSubscriptionView(this.controller, {super.key});
|
||||||
SettingsSubscriptionView(this.controller, {super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final String currentSubscriptionTitle = pangeaController
|
|
||||||
.subscriptionController.subscription?.currentSubscription
|
|
||||||
?.displayName(context) ??
|
|
||||||
"";
|
|
||||||
final String currentSubscriptionPrice = pangeaController
|
|
||||||
.subscriptionController.subscription?.currentSubscription
|
|
||||||
?.displayPrice(context) ??
|
|
||||||
"";
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
|
|
@ -38,17 +23,15 @@ class SettingsSubscriptionView extends StatelessWidget {
|
||||||
body: ListTileTheme(
|
body: ListTileTheme(
|
||||||
iconColor: Theme.of(context).textTheme.bodyLarge!.color,
|
iconColor: Theme.of(context).textTheme.bodyLarge!.color,
|
||||||
child: MaxWidthBody(
|
child: MaxWidthBody(
|
||||||
child: !(pangeaController.subscriptionController.isSubscribed)
|
child: !(controller.subscriptionController.isSubscribed)
|
||||||
? ChangeSubscription(controller: controller)
|
? ChangeSubscription(controller: controller)
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
if (pangeaController.subscriptionController.subscription!
|
if (controller.currentSubscriptionAvailable)
|
||||||
.currentSubscription !=
|
|
||||||
null)
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10n.of(context)!.currentSubscription),
|
title: Text(L10n.of(context)!.currentSubscription),
|
||||||
subtitle: Text(currentSubscriptionTitle),
|
subtitle: Text(controller.currentSubscriptionTitle),
|
||||||
trailing: Text(currentSubscriptionPrice),
|
trailing: Text(controller.currentSubscriptionPrice),
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -84,8 +67,6 @@ class SettingsSubscriptionView extends StatelessWidget {
|
||||||
if (!(controller.showManagementOptions))
|
if (!(controller.showManagementOptions))
|
||||||
ManagementNotAvailableWarning(
|
ManagementNotAvailableWarning(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
subscriptionController:
|
|
||||||
pangeaController.subscriptionController,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -97,11 +78,9 @@ class SettingsSubscriptionView extends StatelessWidget {
|
||||||
|
|
||||||
class ManagementNotAvailableWarning extends StatelessWidget {
|
class ManagementNotAvailableWarning extends StatelessWidget {
|
||||||
final SubscriptionManagementController controller;
|
final SubscriptionManagementController controller;
|
||||||
final SubscriptionController subscriptionController;
|
|
||||||
|
|
||||||
const ManagementNotAvailableWarning({
|
const ManagementNotAvailableWarning({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.subscriptionController,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -112,7 +91,7 @@ class ManagementNotAvailableWarning extends StatelessWidget {
|
||||||
if (controller.isNewUserTrial) {
|
if (controller.isNewUserTrial) {
|
||||||
return L10n.of(context)!.trialExpiration(
|
return L10n.of(context)!.trialExpiration(
|
||||||
formatter.format(
|
formatter.format(
|
||||||
subscriptionController.subscription!.expirationDate!,
|
controller.subscriptionController.subscription!.expirationDate!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -125,13 +104,14 @@ class ManagementNotAvailableWarning extends StatelessWidget {
|
||||||
return warningText;
|
return warningText;
|
||||||
}
|
}
|
||||||
if (controller.currentSubscriptionIsPromotional) {
|
if (controller.currentSubscriptionIsPromotional) {
|
||||||
if (subscriptionController.subscription?.isLifetimeSubscription ??
|
if (controller
|
||||||
|
.subscriptionController.subscription?.isLifetimeSubscription ??
|
||||||
false) {
|
false) {
|
||||||
return L10n.of(context)!.promotionalSubscriptionDesc;
|
return L10n.of(context)!.promotionalSubscriptionDesc;
|
||||||
}
|
}
|
||||||
return L10n.of(context)!.promoSubscriptionExpirationDesc(
|
return L10n.of(context)!.promoSubscriptionExpirationDesc(
|
||||||
formatter.format(
|
formatter.format(
|
||||||
subscriptionController.subscription!.expirationDate!,
|
controller.subscriptionController.subscription!.expirationDate!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import 'dart:developer';
|
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:fluffychat/pangea/widgets/igc/span_card.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -40,6 +43,19 @@ class PangeaTextController extends TextEditingController {
|
||||||
debugger(when: kDebugMode);
|
debugger(when: kDebugMode);
|
||||||
return;
|
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;
|
if (choreographer.igc.igcTextData == null) return;
|
||||||
|
|
||||||
// debugPrint(
|
// debugPrint(
|
||||||
|
|
@ -113,7 +129,20 @@ class PangeaTextController extends TextEditingController {
|
||||||
// debugPrint("composing after ${value.composing.textAfter(value.text)}");
|
// 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);
|
return TextSpan(text: text, style: style);
|
||||||
} else {
|
} else {
|
||||||
final parts = text.split(choreographer.igc.igcTextData!.originalInput);
|
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/pangea_controller.dart';
|
||||||
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
|
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
|
||||||
import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart';
|
import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
||||||
class SubscriptionButtons extends StatelessWidget {
|
class SubscriptionButtons extends StatelessWidget {
|
||||||
final SubscriptionManagementController controller;
|
final SubscriptionManagementController controller;
|
||||||
|
|
@ -17,43 +15,40 @@ class SubscriptionButtons extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final bool inTrialWindow = pangeaController.userController.inTrialWindow;
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: pangeaController
|
itemCount: controller
|
||||||
.subscriptionController.subscription!.availableSubscriptions.length,
|
.subscriptionController.subscription!.availableSubscriptions.length,
|
||||||
itemBuilder: (BuildContext context, int i) => Column(
|
itemBuilder: (BuildContext context, int i) {
|
||||||
children: [
|
final SubscriptionDetails subscription = pangeaController
|
||||||
ListTile(
|
.subscriptionController.subscription!.availableSubscriptions[i];
|
||||||
title: pangeaController.subscriptionController.subscription!
|
return Column(
|
||||||
.availableSubscriptions[i].isTrial
|
children: [
|
||||||
? Text(L10n.of(context)!.oneWeekTrial)
|
ListTile(
|
||||||
: Text(
|
title: subscription.isTrial
|
||||||
pangeaController.subscriptionController.subscription!
|
? Text(L10n.of(context)!.oneWeekTrial)
|
||||||
.availableSubscriptions[i]
|
: Text(
|
||||||
.displayName(context),
|
subscription.displayName(context),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
pangeaController.subscriptionController.subscription!
|
subscription.isTrial && !inTrialWindow
|
||||||
.availableSubscriptions[i]
|
? L10n.of(context)!.trialPeriodExpired
|
||||||
.displayPrice(context),
|
: 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),
|
const Divider(height: 1),
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,68 +18,121 @@ class SubscriptionOptions extends StatelessWidget {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
children: pangeaController
|
spacing: 10,
|
||||||
.subscriptionController.subscription!.availableSubscriptions
|
children: pangeaController.userController.inTrialWindow
|
||||||
.map(
|
? [
|
||||||
(subscription) => SubscriptionCard(
|
SubscriptionCard(
|
||||||
subscription: subscription,
|
onTap: () => pangeaController.subscriptionController
|
||||||
pangeaController: pangeaController,
|
.submitSubscriptionChange(
|
||||||
),
|
SubscriptionDetails(
|
||||||
)
|
price: 0,
|
||||||
.toList(),
|
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 {
|
class SubscriptionCard extends StatelessWidget {
|
||||||
final SubscriptionDetails subscription;
|
final SubscriptionDetails? subscription;
|
||||||
final PangeaController pangeaController;
|
final void Function()? onTap;
|
||||||
|
final String? title;
|
||||||
|
final String? description;
|
||||||
|
final String? buttonText;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
const SubscriptionCard({
|
const SubscriptionCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.subscription,
|
this.subscription,
|
||||||
required this.pangeaController,
|
required this.onTap,
|
||||||
|
this.title,
|
||||||
|
this.description,
|
||||||
|
this.buttonText,
|
||||||
|
this.enabled = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Card(
|
||||||
shape: RoundedRectangleBorder(
|
color: enabled ? null : const Color.fromARGB(255, 245, 244, 244),
|
||||||
side: BorderSide(
|
shape: const RoundedRectangleBorder(
|
||||||
color: AppConfig.primaryColorLight.withAlpha(64),
|
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||||
),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.zero),
|
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 250,
|
width: AppConfig.columnWidth * 0.6,
|
||||||
width: AppConfig.columnWidth * 0.75,
|
height: 200,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(25),
|
padding: const EdgeInsets.all(25),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
subscription.isTrial
|
title ?? subscription?.displayName(context) ?? '',
|
||||||
? L10n.of(context)!.oneWeekTrial
|
|
||||||
: subscription.displayName(context),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(fontSize: 24),
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
color:
|
||||||
|
enabled ? null : const Color.fromARGB(255, 174, 174, 174),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
subscription.displayPrice(context),
|
description ?? subscription?.displayPrice(context) ?? '',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
enabled ? null : const Color.fromARGB(255, 174, 174, 174),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: enabled
|
||||||
pangeaController.subscriptionController
|
? () {
|
||||||
.submitSubscriptionChange(
|
if (onTap != null) onTap!();
|
||||||
subscription,
|
Navigator.of(context).pop();
|
||||||
context,
|
}
|
||||||
);
|
: null,
|
||||||
Navigator.of(context).pop();
|
style: buttonStyle,
|
||||||
},
|
child: Text(
|
||||||
child: Text(L10n.of(context)!.subscribe),
|
buttonText ?? L10n.of(context)!.subscribe,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// Flutter imports:
|
// Flutter imports:
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
|
||||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||||
import 'package:fluffychat/pangea/widgets/subscription/subscription_options.dart';
|
import 'package:fluffychat/pangea/widgets/subscription/subscription_options.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -18,89 +17,38 @@ class SubscriptionPaywall extends StatelessWidget {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
leading: CloseButton(onPressed: Navigator.of(context).pop),
|
leading: const CloseButton(),
|
||||||
title: Text(
|
title: Text(
|
||||||
L10n.of(context)!.getAccess,
|
L10n.of(context)!.getAccess,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 20),
|
||||||
fontSize: 20,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(20),
|
child: Padding(
|
||||||
child: ListView(
|
padding: const EdgeInsets.all(20),
|
||||||
children: [
|
child: Column(
|
||||||
if (pangeaController.matrixState.client.rooms.length > 1) ...[
|
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(
|
Text(
|
||||||
L10n.of(context)!.welcomeBack,
|
L10n.of(context)!.subscriptionDesc,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(fontSize: 16),
|
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ dependencies:
|
||||||
open_file: ^3.3.2
|
open_file: ^3.3.2
|
||||||
purchases_flutter: ^5.6.0
|
purchases_flutter: ^5.6.0
|
||||||
sentry_flutter: ^7.4.0
|
sentry_flutter: ^7.4.0
|
||||||
|
shimmer: ^3.0.0
|
||||||
syncfusion_flutter_datepicker: ^23.2.7
|
syncfusion_flutter_datepicker: ^23.2.7
|
||||||
syncfusion_flutter_xlsio: ^23.2.7
|
syncfusion_flutter_xlsio: ^23.2.7
|
||||||
# Pangea#
|
# Pangea#
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue