You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fluffychat/lib/pangea/controllers/subscription_controller.dart

309 lines
9.6 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/controllers/base_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/base_subscription_info.dart';
import 'package:fluffychat/pangea/models/mobile_subscriptions.dart';
import 'package:fluffychat/pangea/models/web_subscriptions.dart';
import 'package:fluffychat/pangea/network/requests.dart';
import 'package:fluffychat/pangea/network/urls.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/widgets/subscription/subscription_paywall.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:http/http.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SubscriptionController extends BaseController {
late PangeaController _pangeaController;
SubscriptionInfo? subscription;
//convert this logic to use completer
bool initialized = false;
final StreamController subscriptionStream = StreamController.broadcast();
SubscriptionController(PangeaController pangeaController) : super() {
_pangeaController = pangeaController;
}
bool get isSubscribed =>
subscription != null &&
(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) {
debugPrint(
"Attempted to initalize subscription information with null userId",
);
return;
}
subscription = kIsWeb
? WebSubscriptionInfo(pangeaController: _pangeaController)
: MobileSubscriptionInfo(pangeaController: _pangeaController);
await subscription!.configure();
if (activatedNewUserTrial) {
setNewUserTrial();
}
initialized = true;
if (!kIsWeb) {
Purchases.addCustomerInfoUpdateListener(
(CustomerInfo info) async {
final bool wasSubscribed = isSubscribed;
await updateCustomerInfo();
if (!wasSubscribed && isSubscribed) {
subscriptionStream.add(true);
}
},
);
} else {
final bool? beganWebPayment = _pangeaController.pStoreService.read(
PLocalKey.beganWebPayment,
);
if (beganWebPayment ?? false) {
_pangeaController.pStoreService.delete(PLocalKey.beganWebPayment);
if (_pangeaController.subscriptionController.isSubscribed) {
subscriptionStream.add(true);
}
}
}
setState();
} catch (e, s) {
debugPrint("Failed to initialize subscription controller");
ErrorHandler.logError(e: e, s: s);
}
}
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;
await showModalBottomSheet(
isScrollControlled: true,
useRootNavigator: !PlatformInfos.isMobile,
clipBehavior: Clip.hardEdge,
context: context,
constraints: BoxConstraints(
maxHeight: PlatformInfos.isMobile ? 600 : 480,
),
builder: (_) => 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 (kIsWeb) {
if (selectedSubscription.duration == null) {
ErrorHandler.logError(
m: "Tried to subscribe to web SubscriptionDetails with Null duration",
s: StackTrace.current,
);
return;
}
final String paymentLink = await getPaymentLink(
selectedSubscription.duration!,
isPromo: isPromo,
);
_pangeaController.pStoreService.save(
PLocalKey.beganWebPayment,
true,
);
setState();
launchUrlString(
paymentLink,
webOnlyWindowName: "_self",
);
return;
}
if (selectedSubscription.package == null) {
ErrorHandler.logError(
m: "Tried to subscribe to SubscriptionDetails with Null revenuecat Package",
s: StackTrace.current,
);
return;
}
try {
GoogleAnalytics.beginPurchaseSubscription(
selectedSubscription,
context,
);
await Purchases.purchasePackage(selectedSubscription.package!);
GoogleAnalytics.updateUserSubscriptionStatus(true);
} catch (err) {
ErrorHandler.logError(
m: "Failed to purchase revenuecat package for user ${_pangeaController.matrixState.client.userID}",
s: StackTrace.current,
);
return;
}
}
}
Future<void> redeemPromoCode(BuildContext context) async {
final List<String>? promoCode = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.enterPromoCode,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,
textFields: [const DialogTextField()],
);
if (promoCode == null || promoCode.single.isEmpty) return;
launchUrlString(
"${AppConfig.iosPromoCode}${promoCode.single}",
);
}
}
class SubscriptionDetails {
double price;
String? duration;
Package? package;
String? appId;
final String id;
String? periodType = "normal";
SubscriptionDetails({
required this.price,
required this.id,
this.duration,
this.package,
this.appId,
this.periodType,
});
void makeTrial() => periodType = 'trial';
bool get isTrial => periodType == 'trial';
String displayPrice(BuildContext context) {
if (isTrial || price <= 0) {
return L10n.of(context)!.freeTrial;
}
return "\$${price.toStringAsFixed(2)}";
}
String displayName(BuildContext context) {
if (isTrial) {
return L10n.of(context)!.oneWeekTrial;
}
switch (duration) {
case ('month'):
return L10n.of(context)!.monthlySubscription;
case ('year'):
return L10n.of(context)!.yearlySubscription;
default:
return L10n.of(context)!.defaultSubscription;
}
}
}