mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-07-15 07:56:11 +08:00

The accounts setup is taking way way too long. And some ios users have paid for pro and haven't gotten it. This way, they will at least get it ASAP. I should have done this weeks ago.
277 lines
7.6 KiB
Dart
277 lines
7.6 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io' show Platform;
|
|
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
|
import 'package:in_app_purchase/store_kit_wrappers.dart';
|
|
import 'package:meta/meta.dart';
|
|
|
|
import 'package:gitjournal/app.dart';
|
|
import 'package:gitjournal/app_settings.dart';
|
|
import 'package:gitjournal/error_reporting.dart';
|
|
import 'package:gitjournal/features.dart';
|
|
import 'package:gitjournal/utils/logger.dart';
|
|
|
|
class InAppPurchases {
|
|
static Future<void> confirmProPurchaseBoot() async {
|
|
clearTransactionsIos();
|
|
confirmPendingPurchases();
|
|
|
|
if (Features.alwaysPro || !AppSettings.instance.validateProMode) {
|
|
return;
|
|
}
|
|
|
|
if (AppSettings.instance.proMode == false) {
|
|
Log.i("confirmProPurchaseBoot: Pro Mode is false");
|
|
return;
|
|
}
|
|
|
|
var currentDt = DateTime.now().toUtc().toIso8601String();
|
|
var exp = AppSettings.instance.proExpirationDate;
|
|
|
|
Log.i("Checking if ProMode should be enabled. Exp: $exp");
|
|
if (exp != null && exp.isNotEmpty && exp.compareTo(currentDt) > 0) {
|
|
Log.i("Not checking PurchaseInfo as exp = $exp and cur = $currentDt");
|
|
return;
|
|
}
|
|
|
|
if (JournalApp.isInDebugMode) {
|
|
Log.d("Ignoring IAP pro check - debug mode");
|
|
return;
|
|
}
|
|
|
|
return confirmProPurchase();
|
|
}
|
|
|
|
static Future<void> confirmProPurchase() async {
|
|
SubscriptionStatus sub;
|
|
|
|
Log.i("Trying to confirmProPurchase");
|
|
try {
|
|
sub = await _subscriptionStatus();
|
|
if (sub == null) {
|
|
Log.i("Failed to get subscription status");
|
|
return;
|
|
}
|
|
} catch (e, stackTrace) {
|
|
Log.e("Failed to get subscription status", ex: e, stacktrace: stackTrace);
|
|
Log.i("Disabling Pro mode as it has probably expired");
|
|
|
|
AppSettings.instance.proMode = false;
|
|
AppSettings.instance.proExpirationDate = "";
|
|
AppSettings.instance.save();
|
|
|
|
return;
|
|
}
|
|
|
|
Log.i("SubscriptionState: $sub");
|
|
|
|
var isPro = sub.isPro;
|
|
var expiryDate = sub.expiryDate.toIso8601String();
|
|
Log.i("Pro ExpiryDate: $expiryDate");
|
|
|
|
if (AppSettings.instance.proMode != isPro) {
|
|
Log.i("Pro mode changed to $isPro");
|
|
AppSettings.instance.proMode = isPro;
|
|
AppSettings.instance.proExpirationDate = expiryDate;
|
|
AppSettings.instance.save();
|
|
} else {
|
|
AppSettings.instance.proExpirationDate = expiryDate;
|
|
AppSettings.instance.save();
|
|
}
|
|
}
|
|
|
|
static Future<SubscriptionStatus> _subscriptionStatus() async {
|
|
InAppPurchaseConnection.enablePendingPurchases();
|
|
var iapConn = InAppPurchaseConnection.instance;
|
|
var dtNow = DateTime.now().toUtc();
|
|
|
|
var response = await iapConn.queryPastPurchases();
|
|
Log.i("Number of Past Purchases: ${response.pastPurchases.length}");
|
|
|
|
var subs = <SubscriptionStatus>[];
|
|
for (var purchase in response.pastPurchases) {
|
|
DateTime dt;
|
|
try {
|
|
dt = await getExpiryDate(
|
|
purchase.verificationData.serverVerificationData,
|
|
purchase.productID,
|
|
_isPurchase(purchase));
|
|
} catch (e) {
|
|
// Ignore
|
|
}
|
|
|
|
if (dt == null || !dt.isAfter(dtNow)) {
|
|
continue;
|
|
}
|
|
|
|
var sub = SubscriptionStatus(true, dt);
|
|
Log.i("--> $sub");
|
|
subs.add(sub);
|
|
}
|
|
Log.i("Number of SubscriptionStatus: ${subs.length}");
|
|
|
|
var sub = SubscriptionStatus(false, dtNow);
|
|
for (var s in subs) {
|
|
if (s.expiryDate.isAfter(sub.expiryDate)) {
|
|
sub = s;
|
|
}
|
|
}
|
|
|
|
return sub;
|
|
}
|
|
|
|
static Future<void> clearTransactionsIos() async {
|
|
if (!Platform.isIOS) {
|
|
return;
|
|
}
|
|
|
|
final transactions = await SKPaymentQueueWrapper().transactions();
|
|
Log.i("Old Transactions: ${transactions.length}");
|
|
for (final transaction in transactions) {
|
|
Log.i("Processing old transaction: $transaction");
|
|
try {
|
|
if (transaction.transactionState ==
|
|
SKPaymentTransactionStateWrapper.purchased) {
|
|
Log.i("Already purchased. Ignoring");
|
|
continue;
|
|
}
|
|
if (transaction.transactionState ==
|
|
SKPaymentTransactionStateWrapper.restored) {
|
|
Log.i("Already Restored. Ignoring");
|
|
continue;
|
|
}
|
|
|
|
if (transaction.transactionState !=
|
|
SKPaymentTransactionStateWrapper.purchasing) {
|
|
Log.i("Purchasing. Finishing Transaction.");
|
|
|
|
await SKPaymentQueueWrapper().finishTransaction(transaction);
|
|
}
|
|
} catch (e, stackTrace) {
|
|
logException(e, stackTrace);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void confirmPendingPurchases() async {
|
|
// On iOS this results in a "Sign in with Apple ID" dialog
|
|
if (!Platform.isAndroid) {
|
|
return;
|
|
}
|
|
|
|
InAppPurchaseConnection.enablePendingPurchases();
|
|
final iapCon = InAppPurchaseConnection.instance;
|
|
|
|
var pastPurchases = await iapCon.queryPastPurchases();
|
|
for (var pd in pastPurchases.pastPurchases) {
|
|
if (pd.pendingCompletePurchase) {
|
|
Log.i("Pending Complete Purchase - ${pd.productID}");
|
|
|
|
try {
|
|
await iapCon.completePurchase(pd);
|
|
} catch (e, stackTrace) {
|
|
logException(e, stackTrace);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const base_url = 'https://us-central1-gitjournal-io.cloudfunctions.net';
|
|
const ios_url = '$base_url/IAPIosVerify';
|
|
const android_url = '$base_url/IAPAndroidVerify';
|
|
|
|
Future<DateTime> getExpiryDate(
|
|
String receipt, String sku, bool isPurchase) async {
|
|
assert(receipt.isNotEmpty);
|
|
|
|
var body = {
|
|
'receipt': receipt,
|
|
"sku": sku,
|
|
'pseudoId': AppSettings.instance.pseudoId,
|
|
'is_purchase': isPurchase,
|
|
};
|
|
Log.i("getExpiryDate ${json.encode(body)}");
|
|
|
|
var url = Platform.isIOS ? ios_url : android_url;
|
|
var response = await http.post(url, body: json.encode(body));
|
|
if (response.statusCode != 200) {
|
|
Log.e("Received Invalid Status Code from GCP IAP Verify", props: {
|
|
"code": response.statusCode,
|
|
"body": response.body,
|
|
});
|
|
throw IAPVerifyException(
|
|
code: response.statusCode,
|
|
body: response.body,
|
|
receipt: receipt,
|
|
sku: sku,
|
|
isPurchase: isPurchase,
|
|
);
|
|
}
|
|
|
|
Log.i("IAP Verify body: ${response.body}");
|
|
|
|
var b = json.decode(response.body) as Map;
|
|
if (b == null || !b.containsKey("expiry_date")) {
|
|
Log.e("Received Invalid Body from GCP IAP Verify", props: {
|
|
"code": response.statusCode,
|
|
"body": response.body,
|
|
});
|
|
return null;
|
|
}
|
|
|
|
var expiryDateMs = b['expiry_date'] as int;
|
|
return DateTime.fromMillisecondsSinceEpoch(expiryDateMs, isUtc: true);
|
|
}
|
|
|
|
class SubscriptionStatus {
|
|
final bool isPro;
|
|
final DateTime expiryDate;
|
|
|
|
SubscriptionStatus(this.isPro, this.expiryDate);
|
|
|
|
@override
|
|
String toString() =>
|
|
"SubscriptionStatus{isPro: $isPro, expiryDate: $expiryDate}";
|
|
}
|
|
|
|
Future<SubscriptionStatus> verifyPurchase(PurchaseDetails purchase) async {
|
|
var dt = await getExpiryDate(
|
|
purchase.verificationData.serverVerificationData,
|
|
purchase.productID,
|
|
_isPurchase(purchase),
|
|
);
|
|
if (dt == null || !dt.isAfter(DateTime.now())) {
|
|
return SubscriptionStatus(false, dt);
|
|
}
|
|
return SubscriptionStatus(true, dt);
|
|
}
|
|
|
|
// Checks if it is a subscription or a purchase
|
|
bool _isPurchase(PurchaseDetails purchase) {
|
|
var sku = purchase.productID;
|
|
return !sku.contains('monthly') && !sku.contains('_sub_');
|
|
}
|
|
|
|
class IAPVerifyException implements Exception {
|
|
final int code;
|
|
final String body;
|
|
final String receipt;
|
|
final String sku;
|
|
final bool isPurchase;
|
|
|
|
IAPVerifyException({
|
|
@required this.code,
|
|
@required this.body,
|
|
@required this.receipt,
|
|
@required this.sku,
|
|
@required this.isPurchase,
|
|
});
|
|
|
|
@override
|
|
String toString() {
|
|
return "IAPVerifyException{code: $code, body: $body, receipt: $receipt, $sku: sku, isPurchase: $isPurchase}";
|
|
}
|
|
}
|