mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-06-26 16:46:51 +08:00
Move away from RevenueCat
The iOS updates keep getting rejected, and I think it's because RevenueCat is taking too long to respond. Additionally, revenueCat doesn't really give us anything useful as its receipt validation isn't perfect, and I've had to roll my own. Plus from a privacy point of view, this is better as we are no longer talking to any third party service. This has so far only been tested on iOS
This commit is contained in:
@ -105,10 +105,6 @@ PODS:
|
||||
- path_provider_macos (0.0.1):
|
||||
- Flutter
|
||||
- PromisesObjC (1.2.9)
|
||||
- Purchases (3.2.2)
|
||||
- purchases_flutter (1.1.0):
|
||||
- Flutter
|
||||
- Purchases (~> 3.2.2)
|
||||
- quick_actions (0.0.1):
|
||||
- Flutter
|
||||
- Reachability (3.2)
|
||||
@ -152,7 +148,6 @@ DEPENDENCIES:
|
||||
- package_info (from `.symlinks/plugins/package_info/ios`)
|
||||
- path_provider (from `.symlinks/plugins/path_provider/ios`)
|
||||
- path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`)
|
||||
- purchases_flutter (from `.symlinks/plugins/purchases_flutter/ios`)
|
||||
- quick_actions (from `.symlinks/plugins/quick_actions/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- share (from `.symlinks/plugins/share/ios`)
|
||||
@ -179,7 +174,6 @@ SPEC REPOS:
|
||||
- GoogleUtilities
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- Purchases
|
||||
- Reachability
|
||||
- Sentry
|
||||
|
||||
@ -216,8 +210,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider/ios"
|
||||
path_provider_macos:
|
||||
:path: ".symlinks/plugins/path_provider_macos/ios"
|
||||
purchases_flutter:
|
||||
:path: ".symlinks/plugins/purchases_flutter/ios"
|
||||
quick_actions:
|
||||
:path: ".symlinks/plugins/quick_actions/ios"
|
||||
receive_sharing_intent:
|
||||
@ -269,8 +261,6 @@ SPEC CHECKSUMS:
|
||||
path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d
|
||||
path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0
|
||||
PromisesObjC: b48e0338dbbac2207e611750777895f7a5811b75
|
||||
Purchases: 392c729893c011c7d4d37d7f7c9f863e597d4a9b
|
||||
purchases_flutter: 329011208c046353be37f984026815e9dd046456
|
||||
quick_actions: 6cb2390c4dab0e737c94573c27e18d9666710720
|
||||
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
|
||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||
|
18
lib/iap.dart
18
lib/iap.dart
@ -57,6 +57,7 @@ class InAppPurchases {
|
||||
static Future<SubscriptionStatus> _subscriptionStatus() async {
|
||||
InAppPurchaseConnection.enablePendingPurchases();
|
||||
var iapConn = InAppPurchaseConnection.instance;
|
||||
var dtNow = DateTime.now().toUtc();
|
||||
|
||||
if (Platform.isIOS) {
|
||||
var verificationData = await iapConn.refreshPurchaseVerificationData();
|
||||
@ -70,12 +71,12 @@ class InAppPurchases {
|
||||
var dt = await getExpiryDate(
|
||||
purchase.verificationData.serverVerificationData,
|
||||
purchase.productID);
|
||||
if (dt == null || !dt.isAfter(DateTime.now())) {
|
||||
if (dt == null || !dt.isAfter(dtNow)) {
|
||||
continue;
|
||||
}
|
||||
return SubscriptionStatus(true, dt);
|
||||
}
|
||||
return SubscriptionStatus(false, DateTime.now().toUtc());
|
||||
return SubscriptionStatus(false, dtNow);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -117,7 +118,7 @@ Future<DateTime> getExpiryDate(String receipt, String sku) async {
|
||||
}
|
||||
|
||||
var expiryDateMs = b['expiry_date'] as int;
|
||||
return DateTime.fromMillisecondsSinceEpoch(expiryDateMs);
|
||||
return DateTime.fromMillisecondsSinceEpoch(expiryDateMs, isUtc: true);
|
||||
}
|
||||
|
||||
class SubscriptionStatus {
|
||||
@ -130,3 +131,14 @@ class SubscriptionStatus {
|
||||
String toString() =>
|
||||
"SubscriptionStatus{isPro: $isPro, expiryDate: $expiryDate}";
|
||||
}
|
||||
|
||||
Future<SubscriptionStatus> verifyPurchase(PurchaseDetails purchase) async {
|
||||
var dt = await getExpiryDate(
|
||||
purchase.verificationData.serverVerificationData,
|
||||
purchase.productID,
|
||||
);
|
||||
if (dt == null || !dt.isAfter(DateTime.now())) {
|
||||
return SubscriptionStatus(false, dt);
|
||||
}
|
||||
return SubscriptionStatus(true, dt);
|
||||
}
|
||||
|
@ -5,11 +5,12 @@ import 'package:equatable/equatable.dart';
|
||||
class PaymentInfo extends Equatable {
|
||||
final double value;
|
||||
final String text;
|
||||
final String id;
|
||||
|
||||
PaymentInfo(this.value, this.text);
|
||||
PaymentInfo({@required this.id, @required this.value, @required this.text});
|
||||
|
||||
@override
|
||||
List<Object> get props => [value, text];
|
||||
List<Object> get props => [value, text, id];
|
||||
}
|
||||
|
||||
typedef PaymentSliderChanged = Function(PaymentInfo);
|
||||
|
@ -1,96 +1,71 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:purchases_flutter/purchases_flutter.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
|
||||
import 'package:gitjournal/.env.dart';
|
||||
import 'package:gitjournal/analytics.dart';
|
||||
import 'package:gitjournal/error_reporting.dart';
|
||||
import 'package:gitjournal/iap.dart';
|
||||
import 'package:gitjournal/settings.dart';
|
||||
import 'package:gitjournal/utils/logger.dart';
|
||||
import 'package:gitjournal/widgets/purchase_slider.dart';
|
||||
|
||||
class PurchaseButton extends StatelessWidget {
|
||||
final Package package;
|
||||
final ProductDetails product;
|
||||
|
||||
PurchaseButton(this.package);
|
||||
PurchaseButton(this.product);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var price = package != null ? package.product.priceString : "Dev Mode";
|
||||
var price = product != null ? product.price : "Dev Mode";
|
||||
|
||||
return RaisedButton(
|
||||
child: Text('Subscribe for $price / month'),
|
||||
color: Theme.of(context).primaryColor,
|
||||
padding: const EdgeInsets.fromLTRB(32.0, 16.0, 32.0, 16.0),
|
||||
onPressed: package != null ? () => _handlePurchase(context) : null,
|
||||
onPressed: product != null ? () => _initPurchase(context) : null,
|
||||
);
|
||||
}
|
||||
|
||||
void _handlePurchase(BuildContext context) async {
|
||||
try {
|
||||
var purchaserInfo = await Purchases.purchasePackage(package);
|
||||
var isPro = purchaserInfo.entitlements.all["pro"].isActive;
|
||||
if (isPro) {
|
||||
Settings.instance.proMode = true;
|
||||
Settings.instance.proExpirationDate =
|
||||
purchaserInfo.latestExpirationDate;
|
||||
Settings.instance.save();
|
||||
void _initPurchase(BuildContext context) async {
|
||||
var purchaseParam = PurchaseParam(productDetails: product);
|
||||
var sentSuccess = await InAppPurchaseConnection.instance
|
||||
.buyNonConsumable(purchaseParam: purchaseParam);
|
||||
|
||||
getAnalytics().logEvent(
|
||||
name: "purchase_screen_thank_you",
|
||||
);
|
||||
|
||||
Navigator.of(context).popAndPushNamed('/purchase_thank_you');
|
||||
return;
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
var errorCode = PurchasesErrorHelper.getErrorCode(e);
|
||||
var errorContent = "";
|
||||
switch (errorCode) {
|
||||
case PurchasesErrorCode.purchaseCancelledError:
|
||||
errorContent = "User cancelled";
|
||||
break;
|
||||
|
||||
case PurchasesErrorCode.purchaseNotAllowedError:
|
||||
errorContent = "User not allowed to purchase";
|
||||
break;
|
||||
|
||||
default:
|
||||
errorContent = errorCode.toString();
|
||||
break;
|
||||
}
|
||||
|
||||
var dialog = AlertDialog(
|
||||
title: const Text("Purchase Failed"),
|
||||
content: Text(errorContent),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text("OK"),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (!sentSuccess) {
|
||||
var err = "Failed to send purchase request";
|
||||
var dialog = PurchaseFailedDialog(err);
|
||||
await showDialog(context: context, builder: (context) => dialog);
|
||||
return;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> _generateSkus() {
|
||||
var list = <String>{'sku_monthly_min'};
|
||||
for (var i = 0; i < 50; i++) {
|
||||
list.add("sku_monthly_min$i");
|
||||
}
|
||||
print(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
class PurchaseWidget extends StatefulWidget {
|
||||
@override
|
||||
_PurchaseWidgetState createState() => _PurchaseWidgetState();
|
||||
}
|
||||
|
||||
class _PurchaseWidgetState extends State<PurchaseWidget> {
|
||||
List<Offering> _offerings;
|
||||
Offering _selectedOffering;
|
||||
List<ProductDetails> _products;
|
||||
ProductDetails _selectedProduct;
|
||||
StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||
|
||||
final defaultSku = "sku_monthly_min2";
|
||||
String error = "";
|
||||
bool pendingPurchase = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -99,32 +74,30 @@ class _PurchaseWidgetState extends State<PurchaseWidget> {
|
||||
}
|
||||
|
||||
Future<void> initPlatformState() async {
|
||||
await InAppPurchases.confirmProPurchase();
|
||||
if (Settings.instance.proMode) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
// In parallel check if the purchase has been made
|
||||
InAppPurchases.confirmProPurchase().then((void _) {
|
||||
if (Settings.instance.proMode) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
|
||||
await Purchases.setup(
|
||||
environment['revenueCat'],
|
||||
appUserId: Settings.instance.pseudoId,
|
||||
);
|
||||
final iapCon = InAppPurchaseConnection.instance;
|
||||
|
||||
Offerings offerings;
|
||||
try {
|
||||
offerings = await Purchases.getOfferings();
|
||||
} catch (e, stackTrace) {
|
||||
logExceptionWarning(e, stackTrace);
|
||||
final bool available = await iapCon.isAvailable();
|
||||
if (!available) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
error = "Store cannot be reached";
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var offeringList = offerings.all.values.toList();
|
||||
offeringList.retainWhere((Offering o) => o.identifier.contains("monthly"));
|
||||
offeringList.sort((Offering a, Offering b) =>
|
||||
a.monthly.product.price.compareTo(b.monthly.product.price));
|
||||
Log.i("Offerings: $offeringList");
|
||||
final response = await iapCon.queryProductDetails(_generateSkus());
|
||||
if (response.error != null) {
|
||||
Log.e("IAP queryProductDetails: ${response.error}");
|
||||
}
|
||||
var products = response.productDetails;
|
||||
products.sort((a, b) => a.price.compareTo(b.price));
|
||||
Log.i("Products: ${products.map((e) => '${e.id} ${e.price}')}");
|
||||
|
||||
// If the widget was removed from the tree while the asynchronous platform
|
||||
// message was in flight, we want to discard the reply rather than calling
|
||||
@ -132,38 +105,87 @@ class _PurchaseWidgetState extends State<PurchaseWidget> {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_offerings = offeringList;
|
||||
_selectedOffering = _offerings.isNotEmpty ? _offerings.first : null;
|
||||
_products = products;
|
||||
_selectedProduct = _products.isNotEmpty ? _products.first : null;
|
||||
|
||||
if (_offerings.length > 1) {
|
||||
for (var o in _offerings) {
|
||||
var prod = o.monthly.product;
|
||||
if (prod.identifier == defaultSku) {
|
||||
_selectedOffering = o;
|
||||
if (_products.length > 1) {
|
||||
for (var p in _products) {
|
||||
if (p.id == defaultSku) {
|
||||
_selectedProduct = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var fakePackageJson = {
|
||||
'identifier': 'monthly_fake',
|
||||
'product': {
|
||||
'identifier': 'fake_product',
|
||||
'title': 'Fake Product',
|
||||
'priceString': '0 Fake',
|
||||
'price': 0.0,
|
||||
},
|
||||
};
|
||||
|
||||
var fakeOffer = Offering.fromJson(<String, dynamic>{
|
||||
'identifier': 'monthly_fake_offering',
|
||||
'monthly': fakePackageJson,
|
||||
'availablePackages': [fakePackageJson],
|
||||
});
|
||||
|
||||
_offerings = [fakeOffer];
|
||||
_selectedOffering = _offerings[0];
|
||||
// FIXME: Add a fake product for development
|
||||
}
|
||||
});
|
||||
|
||||
// Start listening for changes
|
||||
final purchaseUpdates = iapCon.purchaseUpdatedStream;
|
||||
_subscription = purchaseUpdates.listen(_listenToPurchaseUpdated);
|
||||
}
|
||||
|
||||
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
|
||||
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
|
||||
if (purchaseDetails.status == PurchaseStatus.pending) {
|
||||
//showPendingUI();
|
||||
Log.i("Pending - ${purchaseDetails.productID}");
|
||||
setState(() {
|
||||
pendingPurchase = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
pendingPurchase = false;
|
||||
});
|
||||
if (purchaseDetails.status == PurchaseStatus.error) {
|
||||
_handleIAPError(purchaseDetails.error);
|
||||
} else if (purchaseDetails.status == PurchaseStatus.purchased) {
|
||||
var subStatus = await verifyPurchase(purchaseDetails);
|
||||
if (subStatus.isPro) {
|
||||
_deliverProduct(subStatus);
|
||||
} else {
|
||||
_handleError("Failed to purchase product");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
await InAppPurchaseConnection.instance.consumePurchase(purchaseDetails);
|
||||
}
|
||||
if (purchaseDetails.pendingCompletePurchase) {
|
||||
await InAppPurchaseConnection.instance
|
||||
.completePurchase(purchaseDetails);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleIAPError(IAPError err) {
|
||||
var msg = "${err.code} - ${err.message} - ${err.details}";
|
||||
_handleError(msg);
|
||||
}
|
||||
|
||||
void _handleError(String err) {
|
||||
var dialog = PurchaseFailedDialog(err);
|
||||
showDialog(context: context, builder: (context) => dialog);
|
||||
}
|
||||
|
||||
void _deliverProduct(SubscriptionStatus status) {
|
||||
Settings.instance.proMode = status.isPro;
|
||||
Settings.instance.proExpirationDate = status.expiryDate.toIso8601String();
|
||||
Settings.instance.save();
|
||||
|
||||
getAnalytics().logEvent(
|
||||
name: "purchase_screen_thank_you",
|
||||
);
|
||||
|
||||
Navigator.of(context).popAndPushNamed('/purchase_thank_you');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -171,20 +193,35 @@ class _PurchaseWidgetState extends State<PurchaseWidget> {
|
||||
if (error.isNotEmpty) {
|
||||
return Text("Failed to load: $error");
|
||||
}
|
||||
return _offerings == null
|
||||
if (pendingPurchase) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
return _products == null
|
||||
? const CircularProgressIndicator()
|
||||
: buildBody(context);
|
||||
}
|
||||
|
||||
PaymentInfo _fromOffering(Offering o) {
|
||||
var prod = o.monthly.product;
|
||||
return PaymentInfo(prod.price, prod.priceString);
|
||||
PaymentInfo _fromProductDetail(ProductDetails pd) {
|
||||
if (pd == null) return null;
|
||||
|
||||
double value = -1;
|
||||
if (pd.skProduct != null) {
|
||||
value = double.parse(pd.skProduct.price);
|
||||
} else if (pd.skuDetail != null) {
|
||||
value = pd.skuDetail.originalPriceAmountMicros.toDouble() / 100000;
|
||||
}
|
||||
|
||||
return PaymentInfo(
|
||||
id: pd.id,
|
||||
text: pd.price,
|
||||
value: value,
|
||||
);
|
||||
}
|
||||
|
||||
Offering _fromPaymentInfo(PaymentInfo info) {
|
||||
for (var o in _offerings) {
|
||||
if (o.monthly.product.priceString == info.text) {
|
||||
return o;
|
||||
ProductDetails _fromPaymentInfo(PaymentInfo info) {
|
||||
for (var p in _products) {
|
||||
if (p.id == info.id) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
assert(false);
|
||||
@ -193,11 +230,11 @@ class _PurchaseWidgetState extends State<PurchaseWidget> {
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
var slider = PurchaseSlider(
|
||||
values: _offerings.map(_fromOffering).toList(),
|
||||
selectedValue: _fromOffering(_selectedOffering),
|
||||
values: _products.map(_fromProductDetail).toList(),
|
||||
selectedValue: _fromProductDetail(_selectedProduct),
|
||||
onChanged: (PaymentInfo info) {
|
||||
setState(() {
|
||||
_selectedOffering = _fromPaymentInfo(info);
|
||||
_selectedProduct = _fromPaymentInfo(info);
|
||||
});
|
||||
},
|
||||
);
|
||||
@ -210,7 +247,7 @@ class _PurchaseWidgetState extends State<PurchaseWidget> {
|
||||
icon: const Icon(Icons.arrow_left),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_selectedOffering = _prevOffering();
|
||||
_selectedProduct = _prevProduct();
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -219,7 +256,7 @@ class _PurchaseWidgetState extends State<PurchaseWidget> {
|
||||
icon: const Icon(Icons.arrow_right),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_selectedOffering = _nextOffering();
|
||||
_selectedProduct = _nextProduct();
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -227,26 +264,26 @@ class _PurchaseWidgetState extends State<PurchaseWidget> {
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
PurchaseButton(_selectedOffering?.monthly),
|
||||
PurchaseButton(_selectedProduct),
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
);
|
||||
}
|
||||
|
||||
Offering _prevOffering() {
|
||||
for (var i = 0; i < _offerings.length; i++) {
|
||||
if (_offerings[i] == _selectedOffering) {
|
||||
return i > 0 ? _offerings[i - 1] : _offerings[i];
|
||||
ProductDetails _prevProduct() {
|
||||
for (var i = 0; i < _products.length; i++) {
|
||||
if (_products[i] == _selectedProduct) {
|
||||
return i > 0 ? _products[i - 1] : _products[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Offering _nextOffering() {
|
||||
for (var i = 0; i < _offerings.length; i++) {
|
||||
if (_offerings[i] == _selectedOffering) {
|
||||
return i < _offerings.length - 1 ? _offerings[i + 1] : _offerings[i];
|
||||
ProductDetails _nextProduct() {
|
||||
for (var i = 0; i < _products.length; i++) {
|
||||
if (_products[i] == _selectedProduct) {
|
||||
return i < _products.length - 1 ? _products[i + 1] : _products[i];
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,3 +307,23 @@ class _PurchaseSliderButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PurchaseFailedDialog extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
PurchaseFailedDialog(this.text);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("Purchase Failed"),
|
||||
content: Text(text),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: const Text("OK"),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
28
pubspec.lock
28
pubspec.lock
@ -140,7 +140,7 @@ packages:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.5"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -206,6 +206,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -475,7 +482,7 @@ packages:
|
||||
name: json_rpc_2
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.2.0"
|
||||
launch_review:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -582,7 +589,7 @@ packages:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.4"
|
||||
version: "1.7.0"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -652,7 +659,7 @@ packages:
|
||||
name: process
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.12"
|
||||
version: "3.0.13"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -674,13 +681,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.4"
|
||||
purchases_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: purchases_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
quick_actions:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -895,21 +895,21 @@ packages:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.4"
|
||||
version: "1.14.7"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.15"
|
||||
version: "0.2.16"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.4"
|
||||
version: "0.3.7"
|
||||
tool_base:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -46,7 +46,6 @@ dependencies:
|
||||
sentry: ">=3.0.0 <4.0.0"
|
||||
flutter_sentry: ^0.4.2
|
||||
equatable: ^1.1.0
|
||||
purchases_flutter: ^1.1.0
|
||||
cached_network_image: ^2.2.0+1
|
||||
ssh_key: ^0.5.1
|
||||
isolate: ^2.0.3
|
||||
|
@ -4,7 +4,6 @@ import 'dart:io';
|
||||
Future<void> main() async {
|
||||
final config = {
|
||||
'sentry': Platform.environment['SENTRY_DSN'],
|
||||
'revenueCat': Platform.environment['REVENUE_CAT_API_KEY'],
|
||||
};
|
||||
|
||||
final filename = 'lib/.env.dart';
|
||||
|
Reference in New Issue
Block a user