feat: display user's subscription end/refresh date
This commit is contained in:
parent
a7fea620b3
commit
0ab91ccb68
12 changed files with 155 additions and 89 deletions
|
|
@ -5321,5 +5321,9 @@
|
|||
"inviteFriends": "Invite friends",
|
||||
"activityStatsButtonTooltip": "Activity info",
|
||||
"allow": "Allow",
|
||||
"deny": "Deny"
|
||||
"deny": "Deny",
|
||||
"enabledRenewal": "Enable Subscription Renewal",
|
||||
"subscriptionEndsOn": "Subscription Ends On",
|
||||
"subscriptionRenewsOn": "Subscription Renews On",
|
||||
"waitForSubscriptionChanges": "Changes to your subscription may take a moment to reflect in the app."
|
||||
}
|
||||
|
|
@ -525,7 +525,6 @@ class ChatListController extends State<ChatList>
|
|||
|
||||
//#Pangea
|
||||
StreamSubscription? _invitedSpaceSubscription;
|
||||
StreamSubscription? _subscriptionStatusStream;
|
||||
StreamSubscription? _roomCapacitySubscription;
|
||||
//Pangea#
|
||||
|
||||
|
|
@ -613,13 +612,8 @@ class ChatListController extends State<ChatList>
|
|||
}
|
||||
});
|
||||
|
||||
_subscriptionStatusStream ??= MatrixState
|
||||
.pangeaController.subscriptionController.subscriptionStream.stream
|
||||
.listen((event) {
|
||||
if (mounted) {
|
||||
showSubscribedSnackbar(context);
|
||||
}
|
||||
});
|
||||
MatrixState.pangeaController.subscriptionController.subscriptionNotifier
|
||||
.addListener(_onSubscribe);
|
||||
|
||||
// listen for space child updates for any space that is not the active space
|
||||
// so that when the user navigates to the space that was updated, it will
|
||||
|
|
@ -673,6 +667,10 @@ class ChatListController extends State<ChatList>
|
|||
}
|
||||
|
||||
// #Pangea
|
||||
void _onSubscribe() {
|
||||
if (mounted) showSubscribedSnackbar(context);
|
||||
}
|
||||
|
||||
Future<void> _joinInvitedSpaces() async {
|
||||
final invitedSpaces = Matrix.of(context).client.rooms.where(
|
||||
(r) => r.isSpace && r.membership == Membership.invite,
|
||||
|
|
@ -691,8 +689,9 @@ class ChatListController extends State<ChatList>
|
|||
_intentUriStreamSubscription?.cancel();
|
||||
//#Pangea
|
||||
_invitedSpaceSubscription?.cancel();
|
||||
_subscriptionStatusStream?.cancel();
|
||||
_roomCapacitySubscription?.cancel();
|
||||
MatrixState.pangeaController.subscriptionController.subscriptionNotifier
|
||||
.removeListener(_onSubscribe);
|
||||
//Pangea#
|
||||
scrollController.removeListener(_onScroll);
|
||||
super.dispose();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ class PLocalKey {
|
|||
static const String beganWebPayment = "beganWebPayment";
|
||||
static const String dismissedPaywall = 'dismissedPaywall';
|
||||
static const String paywallBackoff = 'paywallBackoff';
|
||||
static const String clickedCancelSubscription = 'clickedCancelSubscription';
|
||||
static const String messagesSinceUpdate = 'messagesSinceLastUpdate';
|
||||
static const String completedActivities = 'completedActivities';
|
||||
static const String justInputtedCode = 'justInputtedCode';
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import 'package:url_launcher/url_launcher_string.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||
|
|
@ -35,13 +34,13 @@ enum SubscriptionStatus {
|
|||
shouldShowPaywall,
|
||||
}
|
||||
|
||||
class SubscriptionController extends BaseController {
|
||||
class SubscriptionController with ChangeNotifier {
|
||||
late PangeaController _pangeaController;
|
||||
|
||||
CurrentSubscriptionInfo? currentSubscriptionInfo;
|
||||
AvailableSubscriptionsInfo? availableSubscriptionInfo;
|
||||
|
||||
final StreamController subscriptionStream = StreamController.broadcast();
|
||||
final ValueNotifier<bool> subscriptionNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
SubscriptionController(PangeaController pangeaController) : super() {
|
||||
_pangeaController = pangeaController;
|
||||
|
|
@ -119,22 +118,20 @@ class SubscriptionController extends BaseController {
|
|||
(CustomerInfo info) async {
|
||||
final bool? wasSubscribed = isSubscribed;
|
||||
await updateCustomerInfo();
|
||||
if (wasSubscribed != null &&
|
||||
!wasSubscribed &&
|
||||
(isSubscribed != null && isSubscribed!)) {
|
||||
subscriptionStream.add(true);
|
||||
if (wasSubscribed == false && isSubscribed == true) {
|
||||
subscriptionNotifier.value = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (SubscriptionManagementRepo.getBeganWebPayment()) {
|
||||
await SubscriptionManagementRepo.removeBeganWebPayment();
|
||||
if (isSubscribed != null && isSubscribed!) {
|
||||
subscriptionStream.add(true);
|
||||
if (isSubscribed == true) {
|
||||
subscriptionNotifier.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
setState(null);
|
||||
notifyListeners();
|
||||
} catch (e, s) {
|
||||
debugPrint("Failed to initialize subscription controller");
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -197,7 +194,6 @@ class SubscriptionController extends BaseController {
|
|||
isPromo: isPromo,
|
||||
);
|
||||
await SubscriptionManagementRepo.setBeganWebPayment();
|
||||
setState(null);
|
||||
launchUrlString(
|
||||
paymentLink,
|
||||
webOnlyWindowName: "_self",
|
||||
|
|
@ -234,7 +230,7 @@ class SubscriptionController extends BaseController {
|
|||
|
||||
Future<void> updateCustomerInfo() async {
|
||||
await currentSubscriptionInfo?.setCurrentSubscription();
|
||||
setState(null);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// if the user is subscribed, returns subscribed
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class CurrentSubscriptionInfo {
|
|||
final AvailableSubscriptionsInfo availableSubscriptionInfo;
|
||||
|
||||
DateTime? expirationDate;
|
||||
DateTime? unsubscribeDetectedAt;
|
||||
String? currentSubscriptionId;
|
||||
|
||||
CurrentSubscriptionInfo({
|
||||
|
|
@ -59,6 +60,9 @@ class CurrentSubscriptionInfo {
|
|||
(currentSubscription?.appId ==
|
||||
availableSubscriptionInfo.appIds?.currentAppId);
|
||||
|
||||
DateTime? get subscriptionEndDate =>
|
||||
unsubscribeDetectedAt == null ? null : expirationDate;
|
||||
|
||||
void resetSubscription() => currentSubscriptionId = null;
|
||||
Future<void> setCurrentSubscription() async {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,10 +104,10 @@ class MobileSubscriptionInfo extends CurrentSubscriptionInfo {
|
|||
expirationDate = activeEntitlement.expirationDate != null
|
||||
? DateTime.parse(activeEntitlement.expirationDate!)
|
||||
: null;
|
||||
unsubscribeDetectedAt = activeEntitlement.unsubscribeDetectedAt != null
|
||||
? DateTime.parse(activeEntitlement.unsubscribeDetectedAt!)
|
||||
: null;
|
||||
|
||||
if (activeEntitlement.periodType == PeriodType.trial) {
|
||||
// We dont use actual trials as it would require adding a CC on devices
|
||||
}
|
||||
if (currentSubscriptionId != null && currentSubscription == null) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(message: "mismatch of productIds and currentSubscriptionID"),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,16 @@ class WebSubscriptionInfo extends CurrentSubscriptionInfo {
|
|||
);
|
||||
|
||||
currentSubscriptionId = rcResponse.currentSubscriptionId;
|
||||
expirationDate = rcResponse.expirationDate;
|
||||
final currentSubscription =
|
||||
rcResponse.allSubscriptions?[currentSubscriptionId];
|
||||
|
||||
if (currentSubscription != null) {
|
||||
expirationDate = DateTime.tryParse(currentSubscription.expiresDate);
|
||||
unsubscribeDetectedAt =
|
||||
currentSubscription.unsubscribeDetectedAt != null
|
||||
? DateTime.parse(currentSubscription.unsubscribeDetectedAt!)
|
||||
: null;
|
||||
}
|
||||
} catch (err) {
|
||||
currentSubscriptionId = AppConfig.errorSubscriptionId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,21 +171,20 @@ class ChangeSubscription extends StatelessWidget {
|
|||
ElevatedButton(
|
||||
onPressed: () => controller
|
||||
.submitChange(subscription),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
controller.loading
|
||||
? const CircularProgressIndicator
|
||||
.adaptive()
|
||||
: Text(
|
||||
child: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
subscription.isTrial
|
||||
? L10n.of(context)
|
||||
.activateTrial
|
||||
: L10n.of(context).pay,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/pangea/subscription/pages/settings_subscription_view.dart';
|
||||
import 'package:fluffychat/pangea/subscription/repo/subscription_management_repo.dart';
|
||||
import 'package:fluffychat/pangea/subscription/utils/subscription_app_id.dart';
|
||||
import 'package:fluffychat/pangea/subscription/widgets/subscription_snackbar.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class SubscriptionManagement extends StatefulWidget {
|
||||
|
|
@ -27,37 +27,25 @@ class SubscriptionManagementController extends State<SubscriptionManagement> {
|
|||
MatrixState.pangeaController.subscriptionController;
|
||||
|
||||
SubscriptionDetails? selectedSubscription;
|
||||
StreamSubscription? _subscriptionStatusStream;
|
||||
bool loading = false;
|
||||
|
||||
late StreamSubscription _settingsSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (!subscriptionController.initCompleter.isCompleted) {
|
||||
subscriptionController.initialize().then((_) => setState(() {}));
|
||||
}
|
||||
|
||||
_settingsSubscription = subscriptionController.stateStream.listen((event) {
|
||||
debugPrint("stateStream event in subscription settings");
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_subscriptionStatusStream ??=
|
||||
subscriptionController.subscriptionStream.stream.listen((_) {
|
||||
showSubscribedSnackbar(context);
|
||||
context.go('/rooms');
|
||||
});
|
||||
|
||||
subscriptionController.addListener(_onSubscriptionUpdate);
|
||||
subscriptionController.subscriptionNotifier.addListener(_onSubscribe);
|
||||
subscriptionController.updateCustomerInfo();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
subscriptionController.subscriptionNotifier.removeListener(_onSubscribe);
|
||||
subscriptionController.removeListener(_onSubscriptionUpdate);
|
||||
super.dispose();
|
||||
_settingsSubscription.cancel();
|
||||
_subscriptionStatusStream?.cancel();
|
||||
}
|
||||
|
||||
bool get subscriptionsAvailable =>
|
||||
|
|
@ -106,29 +94,46 @@ class SubscriptionManagementController extends State<SubscriptionManagement> {
|
|||
.currentSubscriptionInfo!.currentPlatformMatchesPurchasePlatform;
|
||||
}
|
||||
|
||||
DateTime? get expirationDate =>
|
||||
subscriptionController.currentSubscriptionInfo?.expirationDate;
|
||||
|
||||
DateTime? get subscriptionEndDate =>
|
||||
subscriptionController.currentSubscriptionInfo?.subscriptionEndDate;
|
||||
|
||||
void _onSubscriptionUpdate() => setState(() {});
|
||||
void _onSubscribe() => showSubscribedSnackbar(context);
|
||||
|
||||
Future<void> submitChange(
|
||||
SubscriptionDetails subscription, {
|
||||
bool isPromo = false,
|
||||
}) async {
|
||||
setState(() => loading = true);
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async => subscriptionController.submitSubscriptionChange(
|
||||
try {
|
||||
await subscriptionController.submitSubscriptionChange(
|
||||
subscription,
|
||||
context,
|
||||
isPromo: isPromo,
|
||||
),
|
||||
onError: (error, s) {
|
||||
setState(() => loading = false);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
if (mounted && loading) {
|
||||
setState(() => loading = false);
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"subscription_id": subscription.id,
|
||||
"is_promo": isPromo,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onClickCancelSubscription() async {
|
||||
await SubscriptionManagementRepo.setClickedCancelSubscription();
|
||||
await launchMangementUrl(ManagementOption.cancel);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> launchMangementUrl(ManagementOption option) async {
|
||||
String managementUrl = Environment.stripeManagementUrl;
|
||||
final String? email =
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:intl/intl.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/subscription/pages/change_subscription.dart';
|
||||
import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart';
|
||||
import 'package:fluffychat/pangea/subscription/repo/subscription_management_repo.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
|
||||
class SettingsSubscriptionView extends StatelessWidget {
|
||||
|
|
@ -25,12 +26,18 @@ class SettingsSubscriptionView extends StatelessWidget {
|
|||
Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(L10n.of(context).cancelSubscription),
|
||||
enabled: controller.showManagementOptions,
|
||||
onTap: () => controller.launchMangementUrl(
|
||||
ManagementOption.cancel,
|
||||
title: Text(
|
||||
controller.subscriptionEndDate == null
|
||||
? L10n.of(context).cancelSubscription
|
||||
: L10n.of(context).enabledRenewal,
|
||||
),
|
||||
enabled: controller.showManagementOptions,
|
||||
onTap: controller.onClickCancelSubscription,
|
||||
trailing: Icon(
|
||||
controller.subscriptionEndDate == null
|
||||
? Icons.cancel_outlined
|
||||
: Icons.refresh_outlined,
|
||||
),
|
||||
trailing: const Icon(Icons.cancel_outlined),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
|
|
@ -49,6 +56,42 @@ class SettingsSubscriptionView extends StatelessWidget {
|
|||
),
|
||||
enabled: controller.showManagementOptions,
|
||||
),
|
||||
if (controller.expirationDate != null) ...[
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
controller.subscriptionEndDate != null
|
||||
? L10n.of(context).subscriptionEndsOn
|
||||
: L10n.of(context).subscriptionRenewsOn,
|
||||
),
|
||||
subtitle: Text(
|
||||
DateFormat.yMMMMd().format(
|
||||
controller.expirationDate!.toLocal(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (SubscriptionManagementRepo.getClickedCancelSubscription())
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.info_outline,
|
||||
size: 20,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).waitForSubscriptionChanges,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
];
|
||||
|
|
@ -68,16 +111,15 @@ class SettingsSubscriptionView extends StatelessWidget {
|
|||
child: Column(
|
||||
children: [
|
||||
if (isSubscribed == null)
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
if (isSubscribed != null &&
|
||||
isSubscribed &&
|
||||
!controller.showManagementOptions)
|
||||
const Center(child: CircularProgressIndicator.adaptive())
|
||||
else if (isSubscribed && !controller.showManagementOptions)
|
||||
ManagementNotAvailableWarning(
|
||||
controller: controller,
|
||||
),
|
||||
if (isSubscribed != null && !isSubscribed)
|
||||
)
|
||||
else if (isSubscribed && controller.showManagementOptions)
|
||||
...managementButtons
|
||||
else
|
||||
ChangeSubscription(controller: controller),
|
||||
if (controller.showManagementOptions) ...managementButtons,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -69,4 +69,22 @@ class SubscriptionManagementRepo {
|
|||
final int backoff = _getPaywallBackoff() + 1;
|
||||
await _cache.write(PLocalKey.paywallBackoff, backoff);
|
||||
}
|
||||
|
||||
static Future<void> setClickedCancelSubscription() async {
|
||||
await _cache.write(
|
||||
PLocalKey.clickedCancelSubscription,
|
||||
DateTime.now().toIso8601String(),
|
||||
);
|
||||
}
|
||||
|
||||
static bool getClickedCancelSubscription() {
|
||||
final entry = _cache.read(PLocalKey.clickedCancelSubscription);
|
||||
if (entry == null) return false;
|
||||
final val = DateTime.tryParse(entry);
|
||||
return val != null && DateTime.now().difference(val).inSeconds < 60;
|
||||
}
|
||||
|
||||
static Future<void> removeClickedCancelSubscription() async {
|
||||
await _cache.remove(PLocalKey.clickedCancelSubscription);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,7 +151,6 @@ class RCProductsResponseModel {
|
|||
class RCSubscriptionResponseModel {
|
||||
String? currentSubscriptionId;
|
||||
SubscriptionDetails? currentSubscription;
|
||||
DateTime? expirationDate;
|
||||
List<String>? allEntitlements;
|
||||
Map<String, RCSubscription>? allSubscriptions;
|
||||
|
||||
|
|
@ -159,7 +158,6 @@ class RCSubscriptionResponseModel {
|
|||
this.currentSubscriptionId,
|
||||
this.currentSubscription,
|
||||
this.allEntitlements,
|
||||
this.expirationDate,
|
||||
this.allSubscriptions,
|
||||
});
|
||||
|
||||
|
|
@ -188,14 +186,6 @@ class RCSubscriptionResponseModel {
|
|||
}
|
||||
|
||||
final String currentSubscriptionId = activeEntitlements[0];
|
||||
|
||||
final Map<String, dynamic> currentSubscriptionMetadata =
|
||||
json['subscriptions'][currentSubscriptionId];
|
||||
|
||||
final DateTime expirationDate = DateTime.parse(
|
||||
currentSubscriptionMetadata['expires_date'],
|
||||
);
|
||||
|
||||
final SubscriptionDetails? currentSubscription =
|
||||
allProducts?.firstWhereOrNull(
|
||||
(SubscriptionDetails sub) =>
|
||||
|
|
@ -206,7 +196,6 @@ class RCSubscriptionResponseModel {
|
|||
return RCSubscriptionResponseModel(
|
||||
currentSubscription: currentSubscription,
|
||||
currentSubscriptionId: currentSubscriptionId,
|
||||
expirationDate: expirationDate,
|
||||
allEntitlements: activeEntitlements,
|
||||
allSubscriptions: history,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue